From 2462119defef2d7f28cd5b15b09917c9a46e20b6 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Sun, 6 Feb 2022 18:32:54 +0000 Subject: snapshot --- .gitignore | 1 + Cargo.lock | 286 +++++++++++++++++ Cargo.toml | 13 + src/main.rs | 997 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1297 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..61c1d16 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,286 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "clap" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1132dc3944b31c20dd8b906b3a9f0a5d0243e092d59171414969657ac6aa85" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotup" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "slotmap", + "toml", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ce22ac1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dotup" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.53" +clap = { version = "3.0.14", features = ["derive"] } +serde = { version = "1.0.136", features = ["derive"] } +slotmap = "1.0.6" +toml = "0.5.8" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e2a659b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,997 @@ +#![feature(try_blocks)] +pub mod depot { + use std::{ + collections::HashSet, + ffi::{OsStr, OsString}, + ops::Deref, + path::{Path, PathBuf}, + }; + + use slotmap::SlotMap; + + pub use disk::{read, write}; + + slotmap::new_key_type! {pub struct LinkID;} + slotmap::new_key_type! {struct NodeID;} + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum SearchResult { + Found(LinkID), + Ancestor(LinkID), + NotFound, + } + + #[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 + } + } + + // wrapper for a path under the depot + // this path is relative and does not contain `..` or similar + // Deref(Path) + struct DepotPath(PathBuf); + impl Deref for DepotPath { + type Target = Path; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } + } + + #[derive(Debug, Clone)] + struct Node { + comp: OsString, + parent: NodeID, + kind: NodeKind, + } + + #[derive(Debug, Clone)] + enum NodeKind { + Link(LinkID), + Directory(HashSet), + } + + #[derive(Debug, Clone)] + struct Link { + origin: PathBuf, + destination: PathBuf, + node_id: NodeID, + } + + #[derive(Debug, Clone)] + pub struct Depot { + links: SlotMap, + nodes: SlotMap, + root: NodeID, + } + + impl Default for Depot { + 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 { + links: Default::default(), + nodes, + root, + } + } + } + + impl Depot { + pub fn create( + &mut self, + origin: impl AsRef, + destination: impl AsRef, + ) -> anyhow::Result { + let origin = origin.as_ref(); + let destination = destination.as_ref(); + verify_path(origin)?; + verify_path(destination)?; + + // example + // origin = fish/config.fish + // destination = .config/fish/config.fish + + // search + // if ancestor - return error + // if found - update destination + // if not found - create + + match self.search_unchecked(&origin) { + SearchResult::Found(link_id) => { + let link = &mut self.links[link_id]; + link.destination = destination.to_owned(); + Ok(link_id) + } + SearchResult::Ancestor(_) => Err(anyhow::anyhow!( + "An ancestor of this path is already linked" + )), + SearchResult::NotFound => { + let link_id = self.links.insert(Link { + origin: origin.to_owned(), + destination: destination.to_owned(), + node_id: Default::default(), + }); + let node_id = self.node_create_link(origin, link_id); + self.links[link_id].node_id = node_id; + Ok(link_id) + } + } + } + + pub fn move_link( + &mut self, + link_id: LinkID, + destination: impl AsRef, + ) -> anyhow::Result<()> { + let destination = destination.as_ref(); + verify_path(destination)?; + + let link_node_id = self.links[link_id].node_id; + let link_parent_node_id = self.nodes[link_node_id].parent; + let (node_id, found) = self.node_search(destination); + + // the link is already at the destination + if found && node_id == link_node_id { + return Ok(()); + } + + if found { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(node_link_id) => { + let node_parent_id = node.parent; + let node_link_id = *node_link_id; + assert_ne!(link_id, node_link_id); + self.remove(node_link_id); + self.node_child_remove(link_parent_node_id, link_node_id); + self.node_child_add(node_parent_id, link_node_id); + self.node_set_parent(link_node_id, node_parent_id); + Ok(()) + } + NodeKind::Directory(..) => Err(anyhow::anyhow!( + "Cannot move link, other links exist under the destination" + )), + } + } else { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(..) => Err(anyhow::anyhow!( + "Cannot move link, an ancestor is already linked" + )), + NodeKind::Directory(_) => { + let new_node_id = self.node_create_link(destination, link_id); + self.node_remove(link_node_id); + self.links[link_id].node_id = new_node_id; + Ok(()) + } + } + } + } + + pub fn remove(&mut self, link_id: LinkID) { + let node_id = self.links[link_id].node_id; + self.node_remove(node_id); + self.links.remove(link_id); + } + + pub fn link_view(&self, link_id: LinkID) -> LinkView { + LinkView { + link_id, + depot: self, + } + } + + /// searchs for the link at `origin`. + /// returns SearchResult::Found(..) if there is a link at `origin`. + /// returns SearchResult::Ancestor(..) if an ancestor of `origin` is linked. + /// returns SearchResult::NotFound otherwise. + pub fn search(&self, origin: impl AsRef) -> anyhow::Result { + let origin = origin.as_ref(); + verify_path(origin)?; + Ok(self.search_unchecked(&origin)) + } + + /// returns an iterator for all the links at or under the given path. + pub fn links_under( + &self, + path: impl AsRef, + ) -> anyhow::Result + '_> { + let path = path.as_ref(); + verify_path(path)?; + + let mut link_ids = Vec::new(); + if let Some(node_id) = self.node_find(path) { + let mut node_ids = vec![node_id]; + while let Some(node_id) = node_ids.pop() { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(link_id) => link_ids.push(*link_id), + NodeKind::Directory(children) => node_ids.extend(children.iter().copied()), + } + } + } + Ok(link_ids.into_iter()) + } + + /// returns true if the `path` is a link or contains an ancestor that is linked. + /// returns false otherwise. + pub fn is_linked(&self, path: impl AsRef) -> bool { + match self.search(path) { + Ok(SearchResult::Found(..)) | Ok(SearchResult::Ancestor(..)) => true, + _ => false, + } + } + + fn search_unchecked(&self, origin: &Path) -> SearchResult { + debug_assert!(verify_path(origin).is_ok()); + + let mut origin_comps = iter_path_comps(&origin); + let mut curr_node = self.root; + 'outer: loop { + let node = &self.nodes[curr_node]; + let curr_comp = origin_comps.next(); + match &node.kind { + NodeKind::Link(link_id) => match curr_comp { + Some(_) => break SearchResult::Ancestor(*link_id), + None => break SearchResult::Found(*link_id), + }, + NodeKind::Directory(children) => match curr_comp { + Some(curr_comp) => { + for &child_id in children.iter() { + let child = &self.nodes[child_id]; + if &child.comp == curr_comp { + curr_node = child_id; + continue 'outer; + } + } + break SearchResult::NotFound; + } + None => break SearchResult::NotFound, + }, + } + } + } + + /// creates a new directory node with no children. + /// the node specified by `parent` must be a directory node. + fn node_create_dir_empty(&mut self, parent: NodeID, comp: OsString) -> NodeID { + let node_id = self.nodes.insert(Node { + comp, + parent, + kind: NodeKind::Directory(Default::default()), + }); + self.node_child_add(parent, node_id); + node_id + } + + /// all the nodes up to the node to be created have to be directory nodes. + /// `path` must be a verified path. + fn node_create_link(&mut self, path: &Path, link_id: LinkID) -> NodeID { + assert!(verify_path(path).is_ok()); + let mut curr_node_id = self.root; + let mut path_comps = iter_path_comps(path).peekable(); + // unwrap: a verified path has atleast 1 component + let mut curr_path_comp = path_comps.next().unwrap(); + + while path_comps.peek().is_some() { + let next_node = match self.node_children_search(curr_node_id, curr_path_comp) { + Some(child_id) => child_id, + None => self.node_create_dir_empty(curr_node_id, curr_path_comp.to_owned()), + }; + curr_node_id = next_node; + // unwrap: we known next is Some beacause of this loop's condition + curr_path_comp = path_comps.next().unwrap(); + } + + let new_node = self.nodes.insert(Node { + comp: curr_path_comp.to_owned(), + parent: curr_node_id, + kind: NodeKind::Link(link_id), + }); + self.node_child_add(curr_node_id, new_node); + new_node + } + + /// finds the node at the given path. + /// `path` must be a verified path. + fn node_find(&self, path: &Path) -> Option { + match self.node_search(path) { + (node_id, true) => Some(node_id), + _ => None, + } + } + + /// searches for the node at `path`. if that node does not exists then it returns the + /// closest node. return (closest_node, found) + fn node_search(&self, path: &Path) -> (NodeID, bool) { + debug_assert!(verify_path(path).is_ok()); + + let mut origin_comps = iter_path_comps(&path).peekable(); + let mut curr_node = self.root; + 'outer: loop { + let node = &self.nodes[curr_node]; + match origin_comps.next() { + Some(curr_comp) => match &node.kind { + NodeKind::Link(..) => break (curr_node, false), + NodeKind::Directory(children) => { + for &child_id in children.iter() { + let child = &self.nodes[child_id]; + if &child.comp == curr_comp { + curr_node = child_id; + continue 'outer; + } + } + break (curr_node, false); + } + }, + None => break (curr_node, true), + } + } + } + + /// adds `new_child` to `node_id`'s children. + /// the node specified by `node_id` must be a directory node. + fn node_child_add(&mut self, node_id: NodeID, new_child: NodeID) { + let node = &mut self.nodes[node_id]; + match node.kind { + NodeKind::Directory(ref mut children) => { + children.insert(new_child); + } + _ => unreachable!(), + } + } + + /// searchs for a child with the given comp and returns its id. + /// the node specified by `node_id` must be a directory node. + fn node_children_search(&self, node_id: NodeID, search_comp: &OsStr) -> Option { + let child_ids = match &self.nodes[node_id].kind { + NodeKind::Directory(c) => c, + _ => unreachable!(), + }; + for &child_id in child_ids { + let child = &self.nodes[child_id]; + if child.comp == search_comp { + return Some(child_id); + } + } + None + } + + /// removes `child` from `node_id`'s children. + /// the node specified by `node_id` must be a directory node and it must contain the node + /// `child`. + fn node_child_remove(&mut self, node_id: NodeID, child: NodeID) { + let node = &mut self.nodes[node_id]; + let remove_node = match &mut node.kind { + NodeKind::Directory(children) => { + let contained = children.remove(&child); + assert!(contained); + children.is_empty() + } + _ => unreachable!(), + }; + if remove_node && node_id != self.root { + self.node_remove(node_id); + } + } + + fn node_set_parent(&mut self, node_id: NodeID, parent: NodeID) { + self.nodes[node_id].parent = parent; + } + + fn node_remove(&mut self, node_id: NodeID) { + debug_assert!(node_id != self.root); + debug_assert!(self.nodes.contains_key(node_id)); + + let node = self.nodes.remove(node_id).unwrap(); + match node.kind { + NodeKind::Link(..) => {} + NodeKind::Directory(children) => { + // Right now directory nodes are only removed from inside this function and + // we do not remove directories with children + assert!(children.is_empty()); + } + } + let parent_id = node.parent; + self.node_child_remove(parent_id, node_id); + } + } + + 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 + .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(()) + } + } + + fn verify_path(path: &Path) -> anyhow::Result<()> { + // make sure the path is not empty + // make sure the path is relative + // make sure the path does not contain '.' or '..' + if path.components().next().is_none() { + return Err(anyhow::anyhow!("Path cannot be empty")); + } + for component in path.components() { + match component { + std::path::Component::Prefix(_) => { + return Err(anyhow::anyhow!("Path cannot have prefix")) + } + std::path::Component::RootDir => { + return Err(anyhow::anyhow!("Path must be relative")) + } + std::path::Component::CurDir | std::path::Component::ParentDir => { + return Err(anyhow::anyhow!("Path cannot contain '.' or '..'")) + } + std::path::Component::Normal(_) => {} + } + } + Ok(()) + } + + /// Iterate over the components of a path. + /// # Pre + /// The path can only have "Normal" components. + fn iter_path_comps(path: &Path) -> impl Iterator { + debug_assert!(verify_path(path).is_ok()); + path.components().map(|component| match component { + std::path::Component::Normal(comp) => comp, + _ => unreachable!(), + }) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_depot_create() { + let mut depot = Depot::default(); + depot.create("", "dest1.txt").unwrap_err(); + depot.create("comp1.txt", "").unwrap_err(); + depot.create("", "").unwrap_err(); + + depot.create("comp1.txt", "dest1.txt").unwrap(); + depot.create("comp1.txt", "dest1_updated.txt").unwrap(); + depot + .create("./comp1.txt", "dest1_updated.txt") + .unwrap_err(); + depot.create("/comp1.txt", "dest1.txt").unwrap_err(); + depot.create("dir1/", "destdir1/").unwrap(); + depot.create("dir1/file1.txt", "destfile1.txt").unwrap_err(); + } + + #[test] + fn test_depot_move_link() { + let mut depot = Depot::default(); + let f1 = depot.create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.create("d1/f2", "d1/f2").unwrap(); + + depot.move_link(f1, "d1/f2/f1").unwrap_err(); + depot.move_link(f1, "d1").unwrap_err(); + + depot.move_link(f1, "").unwrap(); + depot.move_link(f1, "d2/f1").unwrap(); + } + + #[test] + fn test_depot_remove() { + let mut depot = Depot::default(); + let f1 = depot.create("d1/f1", "d1/f1").unwrap(); + assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::Found(f1)); + depot.remove(f1); + assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::NotFound); + } + + #[test] + fn test_depot_search() { + let mut depot = Depot::default(); + let f1 = depot.create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.create("d3", "d3").unwrap(); + + assert_eq!(depot.search("d1").unwrap(), SearchResult::NotFound,); + assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::Found(f1),); + assert_eq!(depot.search("d1/f2").unwrap(), SearchResult::Found(f2),); + assert_eq!(depot.search("d1/f3").unwrap(), SearchResult::Found(f3),); + assert_eq!(depot.search("d1/d2/f4").unwrap(), SearchResult::Found(f4),); + assert_eq!(depot.search("d1/d2/f5").unwrap(), SearchResult::NotFound,); + assert_eq!(depot.search("d3/f6").unwrap(), SearchResult::Ancestor(d3),); + } + + #[test] + fn test_iter_path_comps() { + let path = Path::new("comp1/comp2/./comp3/file.txt"); + let mut iter = iter_path_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); + } + } +} + +mod dotup { + use std::{ + collections::HashSet, + path::{Path, PathBuf}, + }; + + use anyhow::Context; + + use crate::depot::{self, Depot, LinkID}; + + #[derive(Debug)] + struct CanonicalPair { + link_id: LinkID, + origin: PathBuf, + destination: PathBuf, + } + + #[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_origin_path(origin.as_ref())?; + let destination = destination.as_ref(); + self.depot.create(origin, destination)?; + }; + match link_result { + Ok(_) => {} + Err(e) => println!("Failed to create link : {e}"), + } + } + + pub fn unlink(&mut self, paths: impl Iterator>) { + for origin in paths { + let unlink_result: anyhow::Result<()> = try { + let origin = self.prepare_origin_path(origin.as_ref())?; + let search_results = self.depot.search(&origin)?; + match search_results { + depot::SearchResult::Found(link_id) => { + self.depot.remove(link_id); + println!("removed link {}", origin.display()); + } + depot::SearchResult::Ancestor(_) | depot::SearchResult::NotFound => { + println!("{} is not linked", origin.display()) + } + } + }; + match unlink_result { + Ok(_) => {} + Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()), + } + } + } + + pub fn install(&self, paths: impl Iterator>) { + let mut already_linked: HashSet = Default::default(); + for origin in paths { + let install_result: anyhow::Result<()> = try { + let origin = self.prepare_origin_path(origin.as_ref())?; + let canonical_pairs = self.canonical_pairs_under(&origin)?; + for pair in canonical_pairs { + if already_linked.contains(&pair.link_id) { + continue; + } + self.install_symlink(&pair.origin, &pair.destination)?; + already_linked.insert(pair.link_id); + } + }; + if let Err(e) = install_result { + println!("error while installing {} : {e}", origin.as_ref().display()); + } + } + } + + pub fn uninstall(&self, paths: impl Iterator>) { + for origin in paths { + let uninstall_result: anyhow::Result<()> = try { + let origin = self.prepare_origin_path(origin.as_ref())?; + let canonical_pairs = self.canonical_pairs_under(&origin)?; + for pair in canonical_pairs { + self.uninstall_symlink(&pair.origin, &pair.destination)?; + } + }; + if let Err(e) = uninstall_result { + println!( + "error while uninstalling {} : {e}", + origin.as_ref().display() + ); + } + } + } + + pub fn mv(&mut self, from: impl Iterator>, to: impl AsRef) { + let to = to.as_ref(); + let from: Vec<_> = from.map(|p| p.as_ref().to_owned()).collect(); + match from.as_slice() { + [] => unreachable!(), + [from] => self.mv_one(from, to), + [from @ ..] => self.mv_many(from, to), + } + } + + fn mv_one(&mut self, from: &Path, to: &Path) {} + + fn mv_many(&mut self, from: &[PathBuf], to: &Path) {} + + fn prepare_origin_path(&self, origin: &Path) -> anyhow::Result { + let canonical = origin + .canonicalize() + .context("Failed to canonicalize origin path")?; + let relative = canonical + .strip_prefix(&self.depot_dir) + .context("Invalid origin path, not under depot directory")?; + Ok(relative.to_owned()) + } + + // returns the canonical pairs (origin, destination) for all links under `path`. + fn canonical_pairs_under(&self, path: &Path) -> anyhow::Result> { + let origin = self.prepare_origin_path(path)?; + let mut paths = Vec::new(); + for link_id in self.depot.links_under(origin)? { + 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); + paths.push(CanonicalPair { + link_id, + origin: canonical_origin, + destination: canonical_destination, + }); + } + Ok(paths) + } + + fn install_symlink(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { + debug_assert!(origin.is_absolute()); + debug_assert!(destination.is_absolute()); + + if let Some(destination_parent) = destination.parent() { + std::fs::create_dir_all(destination_parent) + .context("Failed to create directories")?; + } + + 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 { + std::fs::remove_file(&destination)?; + } + std::os::unix::fs::symlink(origin, destination).context("Failed to create symlink")?; + + Ok(()) + } + + fn uninstall_symlink(&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()?.canonicalize()?; + if symlink_destination == origin { + std::fs::remove_file(&destination)?; + } + } + + Ok(()) + } + } + + 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::path::PathBuf; + + use crate::{ + depot::{self, Depot}, + dotup::{self, Dotup}, + Flags, + }; + + pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; + + 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 read_depot(flags: &Flags) -> anyhow::Result { + let depot_path = depot_path_from_flags(flags)?; + let depot = depot::read(&depot_path)?; + Ok(depot) + } + + pub fn write_depot(flags: &Flags, depot: &Depot) -> anyhow::Result<()> { + let depot_path = depot_path_from_flags(flags)?; + depot::write(&depot_path, depot)?; + Ok(()) + } + + 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")) + } + + fn current_working_directory() -> PathBuf { + std::env::current_dir().expect("Failed to obtain current working directory") + } +} + +use std::path::PathBuf; + +use clap::Parser; +use utils::DEFAULT_DEPOT_FILE_NAME; + +#[derive(Parser, Debug)] +pub struct Flags { + #[clap(long)] + depot: Option, + #[clap(long)] + install_base: Option, +} + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[clap(flatten)] + flags: Flags, + #[clap(subcommand)] + command: SubCommand, +} + +#[derive(Parser, Debug)] +enum SubCommand { + Init(InitArgs), + Link(LinkArgs), + Unlink(UnlinkArgs), + Install(InstallArgs), + Uninstall(UninstallArgs), + Mv(MvArgs), +} + +fn main() -> anyhow::Result<()> { + let args = Args::parse(); + match args.command { + SubCommand::Init(cmd_args) => command_init(args.flags, cmd_args), + SubCommand::Link(cmd_args) => command_link(args.flags, cmd_args), + SubCommand::Unlink(cmd_args) => command_unlink(args.flags, cmd_args), + SubCommand::Install(cmd_args) => command_install(args.flags, cmd_args), + SubCommand::Uninstall(cmd_args) => command_uninstall(args.flags, cmd_args), + SubCommand::Mv(cmd_args) => command_mv(args.flags, cmd_args), + } +} + +#[derive(Parser, Debug)] +struct InitArgs { + path: Option, +} + +fn command_init(_global_flags: Flags, args: InitArgs) -> anyhow::Result<()> { + let depot_path = { + let mut path = args.path.unwrap_or_else(utils::default_depot_path); + if path.is_dir() { + path = path.join(DEFAULT_DEPOT_FILE_NAME); + } + path + }; + + if depot_path.exists() { + println!("Depot at {} already exists", depot_path.display()); + } else { + depot::write(&depot_path, &Default::default())?; + println!("Depot initialized at {}", depot_path.display()); + } + + Ok(()) +} + +#[derive(Parser, Debug)] +struct LinkArgs { + origin: PathBuf, + destination: PathBuf, +} + +fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> { + let mut dotup = utils::read_dotup(&global_flags)?; + dotup.link(args.origin, args.destination); + utils::write_dotup(&dotup)?; + Ok(()) +} + +#[derive(Parser, Debug)] +struct UnlinkArgs { + paths: Vec, +} + +fn command_unlink(global_flags: Flags, args: UnlinkArgs) -> anyhow::Result<()> { + let mut dotup = utils::read_dotup(&global_flags)?; + dotup.unlink(args.paths.into_iter()); + utils::write_dotup(&dotup)?; + Ok(()) +} + +#[derive(Parser, Debug)] +struct InstallArgs { + paths: Vec, +} + +fn command_install(global_flags: Flags, args: InstallArgs) -> anyhow::Result<()> { + let dotup = utils::read_dotup(&global_flags)?; + dotup.install(args.paths.into_iter()); + utils::write_dotup(&dotup)?; + Ok(()) +} + +#[derive(Parser, Debug)] +struct UninstallArgs { + paths: Vec, +} + +fn command_uninstall(global_flags: Flags, args: UninstallArgs) -> anyhow::Result<()> { + let dotup = utils::read_dotup(&global_flags)?; + dotup.uninstall(args.paths.into_iter()); + utils::write_dotup(&dotup)?; + Ok(()) +} + +#[derive(Parser, Debug)] +struct MvArgs { + paths: Vec, +} + +fn command_mv(global_flags: Flags, args: MvArgs) -> anyhow::Result<()> { + let mut dotup = utils::read_dotup(&global_flags)?; + let mut paths = args.paths; + if paths.len() < 2 { + return Err(anyhow::anyhow!("mv requires atleast 2 arguments")); + } + let to = paths.pop().unwrap(); + let from = paths; + dotup.mv(from.iter(), &to); + utils::write_dotup(&dotup)?; + Ok(()) +} -- cgit