diff options
Diffstat (limited to 'src/depot.rs')
| -rw-r--r-- | src/depot.rs | 850 |
1 files changed, 850 insertions, 0 deletions
diff --git a/src/depot.rs b/src/depot.rs new file mode 100644 index 0000000..b2d4e3c --- /dev/null +++ b/src/depot.rs | |||
| @@ -0,0 +1,850 @@ | |||
| 1 | use anyhow::Context; | ||
| 2 | use std::{ | ||
| 3 | collections::HashSet, | ||
| 4 | ffi::{OsStr, OsString}, | ||
| 5 | ops::Index, | ||
| 6 | path::{Path, PathBuf}, | ||
| 7 | }; | ||
| 8 | use thiserror::Error; | ||
| 9 | |||
| 10 | use slotmap::{Key, SlotMap}; | ||
| 11 | |||
| 12 | //pub type Result<T, E = DepotError> = std::result::Result<T, E>; | ||
| 13 | pub use anyhow::Result; | ||
| 14 | pub use disk::{read, write}; | ||
| 15 | |||
| 16 | slotmap::new_key_type! {pub struct LinkID;} | ||
| 17 | slotmap::new_key_type! {struct NodeID;} | ||
| 18 | |||
| 19 | #[derive(Debug, Error)] | ||
| 20 | enum DepotError { | ||
| 21 | #[error("path must be relative")] | ||
| 22 | InvalidPath, | ||
| 23 | #[error("path must be relative and not empty")] | ||
| 24 | InvalidLinkPath, | ||
| 25 | } | ||
| 26 | |||
| 27 | #[derive(Debug, Clone)] | ||
| 28 | struct Node { | ||
| 29 | comp: OsString, | ||
| 30 | parent: NodeID, | ||
| 31 | kind: NodeKind, | ||
| 32 | } | ||
| 33 | |||
| 34 | #[derive(Debug, Clone)] | ||
| 35 | enum NodeKind { | ||
| 36 | Link(LinkID), | ||
| 37 | Directory(HashSet<NodeID>), | ||
| 38 | } | ||
| 39 | |||
| 40 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 41 | enum NodeSearchResult { | ||
| 42 | Found(NodeID), | ||
| 43 | /// the closest NodeID up the the search point. | ||
| 44 | NotFound(NodeID), | ||
| 45 | } | ||
| 46 | |||
| 47 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 48 | pub enum DirNode { | ||
| 49 | Link(LinkID), | ||
| 50 | Directory(PathBuf), | ||
| 51 | } | ||
| 52 | |||
| 53 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 54 | pub enum SearchResult { | ||
| 55 | Found(LinkID), | ||
| 56 | Ancestor(LinkID), | ||
| 57 | NotFound, | ||
| 58 | } | ||
| 59 | |||
| 60 | #[derive(Debug, Clone)] | ||
| 61 | struct Link { | ||
| 62 | origin: PathBuf, | ||
| 63 | destination: PathBuf, | ||
| 64 | origin_id: NodeID, | ||
| 65 | } | ||
| 66 | |||
| 67 | #[derive(Debug)] | ||
| 68 | pub struct LinkView<'a> { | ||
| 69 | link_id: LinkID, | ||
| 70 | depot: &'a Depot, | ||
| 71 | } | ||
| 72 | |||
| 73 | impl<'a> LinkView<'a> { | ||
| 74 | pub fn origin(&self) -> &Path { | ||
| 75 | &self.depot.links[self.link_id].origin | ||
| 76 | } | ||
| 77 | |||
| 78 | pub fn destination(&self) -> &Path { | ||
| 79 | &self.depot.links[self.link_id].destination | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | #[derive(Debug, Clone)] | ||
| 84 | struct DepotTree { | ||
| 85 | root: NodeID, | ||
| 86 | nodes: SlotMap<NodeID, Node>, | ||
| 87 | } | ||
| 88 | |||
| 89 | impl Default for DepotTree { | ||
| 90 | fn default() -> Self { | ||
| 91 | let mut nodes = SlotMap::<NodeID, Node>::default(); | ||
| 92 | let root = nodes.insert(Node { | ||
| 93 | comp: Default::default(), | ||
| 94 | parent: Default::default(), | ||
| 95 | kind: NodeKind::Directory(Default::default()), | ||
| 96 | }); | ||
| 97 | Self { root, nodes } | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | impl Index<NodeID> for DepotTree { | ||
| 102 | type Output = Node; | ||
| 103 | |||
| 104 | fn index(&self, index: NodeID) -> &Self::Output { | ||
| 105 | self.nodes.index(index) | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | impl DepotTree { | ||
| 110 | /// create a node of kind [`NodeKind::Link`]. | ||
| 111 | pub fn link_create(&mut self, path: &Path, link_id: LinkID) -> Result<NodeID> { | ||
| 112 | debug_assert!(path_verify_link(path).is_ok()); | ||
| 113 | |||
| 114 | let path_search_result = self.search(path); | ||
| 115 | |||
| 116 | // handle the error cases | ||
| 117 | match path_search_result { | ||
| 118 | NodeSearchResult::Found(node_id) => { | ||
| 119 | let node = &self.nodes[node_id]; | ||
| 120 | match &node.kind { | ||
| 121 | NodeKind::Link(_) => Err(anyhow::anyhow!("link already exists")), | ||
| 122 | NodeKind::Directory(_) => { | ||
| 123 | Err(anyhow::anyhow!("path already has links under it")) | ||
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | NodeSearchResult::NotFound(ancestor_node_id) => { | ||
| 128 | let ancestor_node = &self.nodes[ancestor_node_id]; | ||
| 129 | match &ancestor_node.kind { | ||
| 130 | NodeKind::Link(_) => Err(anyhow::anyhow!( | ||
| 131 | "an ancestor of this path is already linked" | ||
| 132 | )), | ||
| 133 | NodeKind::Directory(_) => Ok(()), | ||
| 134 | } | ||
| 135 | } | ||
| 136 | }?; | ||
| 137 | |||
| 138 | // create the node | ||
| 139 | // unwrap: this is a verfied link path, it must have atleast one component | ||
| 140 | let filename = path.file_name().unwrap(); | ||
| 141 | let parent_path = path_parent_or_empty(path); | ||
| 142 | let node_id = self.nodes.insert(Node { | ||
| 143 | comp: filename.to_owned(), | ||
| 144 | parent: Default::default(), | ||
| 145 | kind: NodeKind::Link(link_id), | ||
| 146 | }); | ||
| 147 | let parent_id = self.directory_get_or_create(parent_path, node_id); | ||
| 148 | self.nodes[node_id].parent = parent_id; | ||
| 149 | Ok(node_id) | ||
| 150 | } | ||
| 151 | |||
| 152 | pub fn link_update_id(&mut self, node_id: NodeID, link_id: LinkID) { | ||
| 153 | let node = &mut self.nodes[node_id]; | ||
| 154 | match &mut node.kind { | ||
| 155 | NodeKind::Link(lid) => *lid = link_id, | ||
| 156 | NodeKind::Directory(_) => unreachable!(), | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | /// attempts to moves a node of kind [`NodeKind::Link`] to `destination`. | ||
| 161 | pub fn link_move(&mut self, node_id: NodeID, destination: &Path) -> Result<()> { | ||
| 162 | let parent_id = self.nodes[node_id].parent; | ||
| 163 | let parent = &mut self.nodes[parent_id]; | ||
| 164 | |||
| 165 | // remove the node from its parent temporarily so that the search never returns this | ||
| 166 | // link and that way any link will find means an error. | ||
| 167 | // if an error does happen then we re-add this node to its parent to keep the data | ||
| 168 | // consistent. | ||
| 169 | match &mut parent.kind { | ||
| 170 | NodeKind::Link(_) => unreachable!(), | ||
| 171 | NodeKind::Directory(children) => children.remove(&node_id), | ||
| 172 | }; | ||
| 173 | |||
| 174 | let search_result = self.search(destination); | ||
| 175 | // handle the error cases | ||
| 176 | match search_result { | ||
| 177 | NodeSearchResult::Found(found_id) => { | ||
| 178 | assert!(found_id != node_id); | ||
| 179 | self.directory_add_child(parent_id, node_id); | ||
| 180 | return Err(anyhow::anyhow!("link already exists at that path")); | ||
| 181 | } | ||
| 182 | NodeSearchResult::NotFound(ancestor_id) => { | ||
| 183 | let ancestor = &self.nodes[ancestor_id]; | ||
| 184 | match &ancestor.kind { | ||
| 185 | NodeKind::Link(_) => { | ||
| 186 | self.directory_add_child(parent_id, node_id); | ||
| 187 | return Err(anyhow::anyhow!("ancestor path is already linked")); | ||
| 188 | } | ||
| 189 | NodeKind::Directory(_) => {} | ||
| 190 | } | ||
| 191 | } | ||
| 192 | }; | ||
| 193 | |||
| 194 | let destination_parent = path_parent_or_empty(destination); | ||
| 195 | let new_parent_id = self.directory_get_or_create(destination_parent, node_id); | ||
| 196 | if new_parent_id != parent_id { | ||
| 197 | self.nodes[node_id].parent = new_parent_id; | ||
| 198 | |||
| 199 | // we have to re-add and call the remove function because it could lead to the removal | ||
| 200 | // of several directories if they become empty after this remove. | ||
| 201 | self.directory_add_child(parent_id, node_id); | ||
| 202 | self.directory_remove_child(parent_id, node_id); | ||
| 203 | } | ||
| 204 | |||
| 205 | // unwrap: destination is a verified link path so it has atleast 1 component | ||
| 206 | let comp = destination.file_name().unwrap(); | ||
| 207 | let node = &mut self.nodes[node_id]; | ||
| 208 | if node.comp != comp { | ||
| 209 | node.comp = comp.to_owned(); | ||
| 210 | } | ||
| 211 | |||
| 212 | Ok(()) | ||
| 213 | } | ||
| 214 | |||
| 215 | pub fn link_search(&self, path: &Path) -> SearchResult { | ||
| 216 | match self.search(path) { | ||
| 217 | NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { | ||
| 218 | NodeKind::Link(link_id) => SearchResult::Found(*link_id), | ||
| 219 | NodeKind::Directory(_) => SearchResult::NotFound, | ||
| 220 | }, | ||
| 221 | NodeSearchResult::NotFound(node_id) => match &self.nodes[node_id].kind { | ||
| 222 | NodeKind::Link(link_id) => SearchResult::Ancestor(*link_id), | ||
| 223 | NodeKind::Directory(_) => SearchResult::NotFound, | ||
| 224 | }, | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | /// remove a node of kind [`NodeKind::Link`]. | ||
| 229 | pub fn link_remove(&mut self, node_id: NodeID) { | ||
| 230 | let node = &self.nodes[node_id]; | ||
| 231 | assert!(std::matches!(node.kind, NodeKind::Link(_))); | ||
| 232 | let parent_id = node.parent; | ||
| 233 | self.nodes.remove(node_id); | ||
| 234 | self.directory_remove_child(parent_id, node_id); | ||
| 235 | } | ||
| 236 | |||
| 237 | pub fn links_under(&self, path: &Path) -> impl Iterator<Item = LinkID> + '_ { | ||
| 238 | let links = match self.search(path) { | ||
| 239 | NodeSearchResult::Found(node_id) => { | ||
| 240 | let node = &self.nodes[node_id]; | ||
| 241 | match &node.kind { | ||
| 242 | NodeKind::Link(link_id) => vec![*link_id], | ||
| 243 | NodeKind::Directory(children) => { | ||
| 244 | let mut links = Vec::new(); | ||
| 245 | let mut node_ids = Vec::from_iter(children.iter().copied()); | ||
| 246 | while let Some(child_id) = node_ids.pop() { | ||
| 247 | let child = &self.nodes[child_id]; | ||
| 248 | match &child.kind { | ||
| 249 | NodeKind::Link(link_id) => links.push(*link_id), | ||
| 250 | NodeKind::Directory(extra_children) => { | ||
| 251 | node_ids.extend(extra_children.iter().copied()) | ||
| 252 | } | ||
| 253 | } | ||
| 254 | } | ||
| 255 | links | ||
| 256 | } | ||
| 257 | } | ||
| 258 | } | ||
| 259 | NodeSearchResult::NotFound(_) => vec![], | ||
| 260 | }; | ||
| 261 | links.into_iter() | ||
| 262 | } | ||
| 263 | |||
| 264 | pub fn has_links_under(&self, path: &Path) -> bool { | ||
| 265 | // it does not matter what type of node is found. if a directory exists then there | ||
| 266 | // must be atleast one link under it. | ||
| 267 | match self.search(path) { | ||
| 268 | NodeSearchResult::Found(_) => true, | ||
| 269 | NodeSearchResult::NotFound(_) => false, | ||
| 270 | } | ||
| 271 | } | ||
| 272 | |||
| 273 | pub fn read_dir(&self, path: &Path) -> Result<impl Iterator<Item = DirNode> + '_> { | ||
| 274 | match self.search(path) { | ||
| 275 | NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { | ||
| 276 | NodeKind::Link(_) => Err(anyhow::anyhow!("read dir called on a link")), | ||
| 277 | NodeKind::Directory(children) => Ok(children.iter().map(|child_id| { | ||
| 278 | let child = &self.nodes[*child_id]; | ||
| 279 | match &child.kind { | ||
| 280 | NodeKind::Link(link_id) => DirNode::Link(*link_id), | ||
| 281 | NodeKind::Directory(_) => DirNode::Directory(self.build_path(*child_id)), | ||
| 282 | } | ||
| 283 | })), | ||
| 284 | }, | ||
| 285 | NodeSearchResult::NotFound(_) => Err(anyhow::anyhow!("directory not found")), | ||
| 286 | } | ||
| 287 | } | ||
| 288 | |||
| 289 | pub fn build_path(&self, node_id: NodeID) -> PathBuf { | ||
| 290 | fn recursive_helper(nodes: &SlotMap<NodeID, Node>, nid: NodeID, pbuf: &mut PathBuf) { | ||
| 291 | if nid.is_null() { | ||
| 292 | return; | ||
| 293 | } | ||
| 294 | let parent_id = nodes[nid].parent; | ||
| 295 | recursive_helper(nodes, parent_id, pbuf); | ||
| 296 | pbuf.push(&nodes[nid].comp); | ||
| 297 | } | ||
| 298 | |||
| 299 | let mut node_path = PathBuf::default(); | ||
| 300 | recursive_helper(&self.nodes, node_id, &mut node_path); | ||
| 301 | node_path | ||
| 302 | } | ||
| 303 | |||
| 304 | fn search(&self, path: &Path) -> NodeSearchResult { | ||
| 305 | debug_assert!(path_verify(path).is_ok()); | ||
| 306 | |||
| 307 | let mut curr_node_id = self.root; | ||
| 308 | let mut comp_iter = path_iter_comps(path).peekable(); | ||
| 309 | while let Some(comp) = comp_iter.next() { | ||
| 310 | if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { | ||
| 311 | let child = &self.nodes[child_id]; | ||
| 312 | match &child.kind { | ||
| 313 | NodeKind::Link(_) => { | ||
| 314 | if comp_iter.peek().is_some() { | ||
| 315 | return NodeSearchResult::NotFound(child_id); | ||
| 316 | } else { | ||
| 317 | return NodeSearchResult::Found(child_id); | ||
| 318 | } | ||
| 319 | } | ||
| 320 | NodeKind::Directory(_) => curr_node_id = child_id, | ||
| 321 | } | ||
| 322 | } else { | ||
| 323 | return NodeSearchResult::NotFound(curr_node_id); | ||
| 324 | } | ||
| 325 | } | ||
| 326 | NodeSearchResult::Found(curr_node_id) | ||
| 327 | } | ||
| 328 | |||
| 329 | // creates directories all the way up to and including path. | ||
| 330 | // there cannot be any links up to `path`. | ||
| 331 | fn directory_get_or_create(&mut self, path: &Path, initial_child: NodeID) -> NodeID { | ||
| 332 | // TODO: this could be replaced if the search function also returned the depth of the | ||
| 333 | // node and we skip those components and just start creating directories up to the | ||
| 334 | // path. | ||
| 335 | let mut curr_node_id = self.root; | ||
| 336 | for comp in path_iter_comps(path) { | ||
| 337 | if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { | ||
| 338 | debug_assert!(std::matches!( | ||
| 339 | self.nodes[child_id].kind, | ||
| 340 | NodeKind::Directory(_) | ||
| 341 | )); | ||
| 342 | curr_node_id = child_id; | ||
| 343 | } else { | ||
| 344 | let new_node_id = self.nodes.insert(Node { | ||
| 345 | comp: comp.to_owned(), | ||
| 346 | parent: curr_node_id, | ||
| 347 | kind: NodeKind::Directory(Default::default()), | ||
| 348 | }); | ||
| 349 | self.directory_add_child(curr_node_id, new_node_id); | ||
| 350 | curr_node_id = new_node_id; | ||
| 351 | } | ||
| 352 | } | ||
| 353 | self.directory_add_child(curr_node_id, initial_child); | ||
| 354 | curr_node_id | ||
| 355 | } | ||
| 356 | |||
| 357 | fn directory_search_children(&self, node_id: NodeID, comp: &OsStr) -> Option<NodeID> { | ||
| 358 | let node = &self.nodes[node_id]; | ||
| 359 | match &node.kind { | ||
| 360 | NodeKind::Link(_) => unreachable!(), | ||
| 361 | NodeKind::Directory(children) => { | ||
| 362 | for &child_id in children { | ||
| 363 | let child = &self.nodes[child_id]; | ||
| 364 | if child.comp == comp { | ||
| 365 | return Some(child_id); | ||
| 366 | } | ||
| 367 | } | ||
| 368 | } | ||
| 369 | } | ||
| 370 | None | ||
| 371 | } | ||
| 372 | |||
| 373 | fn directory_add_child(&mut self, node_id: NodeID, child_id: NodeID) { | ||
| 374 | let node = &mut self.nodes[node_id]; | ||
| 375 | match &mut node.kind { | ||
| 376 | NodeKind::Link(_) => unreachable!(), | ||
| 377 | NodeKind::Directory(children) => children.insert(child_id), | ||
| 378 | }; | ||
| 379 | } | ||
| 380 | |||
| 381 | fn directory_remove_child(&mut self, node_id: NodeID, child_id: NodeID) { | ||
| 382 | let node = &mut self.nodes[node_id]; | ||
| 383 | match &mut node.kind { | ||
| 384 | NodeKind::Link(_) => unreachable!(), | ||
| 385 | NodeKind::Directory(children) => { | ||
| 386 | children.remove(&child_id); | ||
| 387 | if children.is_empty() && !node.parent.is_null() { | ||
| 388 | let parent_id = node.parent; | ||
| 389 | self.directory_remove_child(parent_id, node_id); | ||
| 390 | } | ||
| 391 | } | ||
| 392 | } | ||
| 393 | } | ||
| 394 | } | ||
| 395 | |||
| 396 | #[derive(Debug, Default, Clone)] | ||
| 397 | pub struct Depot { | ||
| 398 | links: SlotMap<LinkID, Link>, | ||
| 399 | origin: DepotTree, | ||
| 400 | } | ||
| 401 | |||
| 402 | impl Depot { | ||
| 403 | pub fn link_create( | ||
| 404 | &mut self, | ||
| 405 | origin: impl AsRef<Path>, | ||
| 406 | destination: impl AsRef<Path>, | ||
| 407 | ) -> Result<LinkID> { | ||
| 408 | let origin = origin.as_ref(); | ||
| 409 | let destination = destination.as_ref(); | ||
| 410 | path_verify_link(origin)?; | ||
| 411 | path_verify_link(destination)?; | ||
| 412 | self.link_create_unchecked(origin, destination) | ||
| 413 | } | ||
| 414 | |||
| 415 | pub fn link_remove(&mut self, link_id: LinkID) { | ||
| 416 | let node_id = self.links[link_id].origin_id; | ||
| 417 | self.links.remove(link_id); | ||
| 418 | self.origin.link_remove(node_id); | ||
| 419 | } | ||
| 420 | |||
| 421 | /// moves the link specified by `link_id` to the path at `destination`. | ||
| 422 | /// if the link is already at the destination nothing is done. | ||
| 423 | /// if the destination is another link that that link is removed. | ||
| 424 | /// if the destination is under another link then an error is returned. | ||
| 425 | /// `destination` will be the link's new origin. | ||
| 426 | pub fn link_move(&mut self, link_id: LinkID, destination: impl AsRef<Path>) -> Result<()> { | ||
| 427 | let destination = destination.as_ref(); | ||
| 428 | path_verify_link(destination)?; | ||
| 429 | self.link_move_unchecked(link_id, destination) | ||
| 430 | } | ||
| 431 | |||
| 432 | #[allow(unused)] | ||
| 433 | pub fn link_search(&self, path: impl AsRef<Path>) -> Result<SearchResult> { | ||
| 434 | let path = path.as_ref(); | ||
| 435 | path_verify(path)?; | ||
| 436 | Ok(self.link_search_unchecked(path)) | ||
| 437 | } | ||
| 438 | |||
| 439 | pub fn link_find(&self, path: impl AsRef<Path>) -> Result<Option<LinkID>> { | ||
| 440 | let path = path.as_ref(); | ||
| 441 | path_verify(path)?; | ||
| 442 | Ok(self.link_find_unchecked(path)) | ||
| 443 | } | ||
| 444 | |||
| 445 | pub fn links_under(&self, path: impl AsRef<Path>) -> Result<impl Iterator<Item = LinkID> + '_> { | ||
| 446 | let path = path.as_ref(); | ||
| 447 | path_verify(path)?; | ||
| 448 | Ok(self.links_under_unchecked(path)) | ||
| 449 | } | ||
| 450 | |||
| 451 | pub fn has_links_under(&self, path: impl AsRef<Path>) -> Result<bool> { | ||
| 452 | let path = path.as_ref(); | ||
| 453 | path_verify(path)?; | ||
| 454 | Ok(self.has_links_under_unchecked(path)) | ||
| 455 | } | ||
| 456 | |||
| 457 | pub fn links_verify_install(&self, link_ids: impl Iterator<Item = LinkID>) -> Result<()> { | ||
| 458 | let mut destination = DepotTree::default(); | ||
| 459 | for link_id in link_ids { | ||
| 460 | let link = &self.links[link_id]; | ||
| 461 | destination | ||
| 462 | .link_create(&link.destination, link_id) | ||
| 463 | .context("link destinations overlap")?; | ||
| 464 | } | ||
| 465 | Ok(()) | ||
| 466 | } | ||
| 467 | |||
| 468 | pub fn link_view(&self, link_id: LinkID) -> LinkView { | ||
| 469 | LinkView { | ||
| 470 | link_id, | ||
| 471 | depot: self, | ||
| 472 | } | ||
| 473 | } | ||
| 474 | |||
| 475 | pub fn read_dir(&self, path: impl AsRef<Path>) -> Result<impl Iterator<Item = DirNode> + '_> { | ||
| 476 | let path = path.as_ref(); | ||
| 477 | path_verify(path)?; | ||
| 478 | self.read_dir_unchecked(path) | ||
| 479 | } | ||
| 480 | |||
| 481 | fn link_create_unchecked(&mut self, origin: &Path, destination: &Path) -> Result<LinkID> { | ||
| 482 | let node_id = self.origin.link_create(origin, Default::default())?; | ||
| 483 | let link_id = self.links.insert(Link { | ||
| 484 | origin: origin.to_owned(), | ||
| 485 | destination: destination.to_owned(), | ||
| 486 | origin_id: node_id, | ||
| 487 | }); | ||
| 488 | self.origin.link_update_id(node_id, link_id); | ||
| 489 | Ok(link_id) | ||
| 490 | } | ||
| 491 | |||
| 492 | fn link_move_unchecked(&mut self, link_id: LinkID, destination: &Path) -> Result<()> { | ||
| 493 | let link = &self.links[link_id]; | ||
| 494 | if link.origin == destination { | ||
| 495 | return Ok(()); | ||
| 496 | } | ||
| 497 | let node_id = link.origin_id; | ||
| 498 | self.origin.link_move(node_id, destination)?; | ||
| 499 | self.links[link_id].origin = destination.to_owned(); | ||
| 500 | Ok(()) | ||
| 501 | } | ||
| 502 | |||
| 503 | fn link_search_unchecked(&self, path: &Path) -> SearchResult { | ||
| 504 | self.origin.link_search(path) | ||
| 505 | } | ||
| 506 | |||
| 507 | fn link_find_unchecked(&self, path: &Path) -> Option<LinkID> { | ||
| 508 | match self.link_search_unchecked(path) { | ||
| 509 | SearchResult::Found(link_id) => Some(link_id), | ||
| 510 | _ => None, | ||
| 511 | } | ||
| 512 | } | ||
| 513 | |||
| 514 | fn links_under_unchecked(&self, path: &Path) -> impl Iterator<Item = LinkID> + '_ { | ||
| 515 | self.origin.links_under(path) | ||
| 516 | } | ||
| 517 | |||
| 518 | fn has_links_under_unchecked(&self, path: &Path) -> bool { | ||
| 519 | self.origin.has_links_under(path) | ||
| 520 | } | ||
| 521 | |||
| 522 | fn read_dir_unchecked(&self, path: &Path) -> Result<impl Iterator<Item = DirNode> + '_> { | ||
| 523 | self.origin.read_dir(path) | ||
| 524 | } | ||
| 525 | } | ||
| 526 | |||
| 527 | /// a verified link path is a path that: | ||
| 528 | /// + is not empty | ||
| 529 | /// + is relative | ||
| 530 | /// + does not contain Prefix/RootDir/ParentDir | ||
| 531 | fn path_verify_link(path: &Path) -> Result<()> { | ||
| 532 | // make sure the path is not empty | ||
| 533 | if path.components().next().is_none() { | ||
| 534 | return Err(DepotError::InvalidLinkPath.into()); | ||
| 535 | } | ||
| 536 | path_verify(path).map_err(|_| DepotError::InvalidLinkPath.into()) | ||
| 537 | } | ||
| 538 | |||
| 539 | /// a verified path is a path that: | ||
| 540 | /// + is not empty | ||
| 541 | /// + is relative | ||
| 542 | /// + does not contain Prefix/RootDir/ParentDir | ||
| 543 | fn path_verify(path: &Path) -> Result<()> { | ||
| 544 | // make sure the path is relative | ||
| 545 | // make sure the path does not contain '.' or '..' | ||
| 546 | for component in path.components() { | ||
| 547 | match component { | ||
| 548 | std::path::Component::Prefix(_) | ||
| 549 | | std::path::Component::RootDir | ||
| 550 | | std::path::Component::CurDir | ||
| 551 | | std::path::Component::ParentDir => return Err(DepotError::InvalidPath.into()), | ||
| 552 | std::path::Component::Normal(_) => {} | ||
| 553 | } | ||
| 554 | } | ||
| 555 | Ok(()) | ||
| 556 | } | ||
| 557 | |||
| 558 | fn path_parent_or_empty(path: &Path) -> &Path { | ||
| 559 | path.parent().unwrap_or_else(|| Path::new("")) | ||
| 560 | } | ||
| 561 | |||
| 562 | /// Iterate over the components of a path. | ||
| 563 | /// # Pre | ||
| 564 | /// The path can only have "Normal" components. | ||
| 565 | fn path_iter_comps(path: &Path) -> impl Iterator<Item = &OsStr> { | ||
| 566 | debug_assert!(path_verify(path).is_ok()); | ||
| 567 | path.components().map(|component| match component { | ||
| 568 | std::path::Component::Normal(comp) => comp, | ||
| 569 | _ => unreachable!(), | ||
| 570 | }) | ||
| 571 | } | ||
| 572 | |||
| 573 | mod disk { | ||
| 574 | use std::path::{Path, PathBuf}; | ||
| 575 | |||
| 576 | use anyhow::Context; | ||
| 577 | use serde::{Deserialize, Serialize}; | ||
| 578 | |||
| 579 | use super::Depot; | ||
| 580 | |||
| 581 | #[derive(Debug, Serialize, Deserialize)] | ||
| 582 | struct DiskLink { | ||
| 583 | origin: PathBuf, | ||
| 584 | destination: PathBuf, | ||
| 585 | } | ||
| 586 | |||
| 587 | #[derive(Debug, Serialize, Deserialize)] | ||
| 588 | struct DiskLinks { | ||
| 589 | links: Vec<DiskLink>, | ||
| 590 | } | ||
| 591 | |||
| 592 | pub fn read(path: &Path) -> anyhow::Result<Depot> { | ||
| 593 | let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; | ||
| 594 | let disk_links = toml::from_str::<DiskLinks>(&contents) | ||
| 595 | .context("Failed to parse depot file")? | ||
| 596 | .links; | ||
| 597 | let mut depot = Depot::default(); | ||
| 598 | for disk_link in disk_links { | ||
| 599 | depot | ||
| 600 | .link_create(disk_link.origin, disk_link.destination) | ||
| 601 | .context("Failed to build depot from file. File is in an invalid state")?; | ||
| 602 | } | ||
| 603 | Ok(depot) | ||
| 604 | } | ||
| 605 | |||
| 606 | pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { | ||
| 607 | let mut links = Vec::with_capacity(depot.links.len()); | ||
| 608 | for (_, link) in depot.links.iter() { | ||
| 609 | links.push(DiskLink { | ||
| 610 | origin: link.origin.clone(), | ||
| 611 | destination: link.destination.clone(), | ||
| 612 | }); | ||
| 613 | } | ||
| 614 | let contents = | ||
| 615 | toml::to_string_pretty(&DiskLinks { links }).context("Failed to serialize depot")?; | ||
| 616 | std::fs::write(path, contents).context("Failed to write depot to file")?; | ||
| 617 | Ok(()) | ||
| 618 | } | ||
| 619 | } | ||
| 620 | |||
| 621 | #[cfg(test)] | ||
| 622 | mod tests { | ||
| 623 | use super::*; | ||
| 624 | |||
| 625 | #[test] | ||
| 626 | fn test_depot_link_create() { | ||
| 627 | let mut depot = Depot::default(); | ||
| 628 | let f1 = depot.link_create("f1", "f1").unwrap(); | ||
| 629 | let f2 = depot.link_create("f2", "f2").unwrap(); | ||
| 630 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 631 | let f4 = depot.link_create("d1/d2/f4", "d1/d2/d4").unwrap(); | ||
| 632 | |||
| 633 | assert_eq!(depot.link_find("f1").unwrap(), Some(f1)); | ||
| 634 | assert_eq!(depot.link_find("f2").unwrap(), Some(f2)); | ||
| 635 | assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); | ||
| 636 | assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); | ||
| 637 | |||
| 638 | depot.link_create("f2", "").unwrap_err(); | ||
| 639 | depot.link_create("", "d4").unwrap_err(); | ||
| 640 | depot.link_create("f1/f3", "f3").unwrap_err(); | ||
| 641 | } | ||
| 642 | |||
| 643 | #[test] | ||
| 644 | fn test_depot_link_remove() { | ||
| 645 | let mut depot = Depot::default(); | ||
| 646 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 647 | let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 648 | let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 649 | let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 650 | let d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 651 | |||
| 652 | depot.link_remove(f2); | ||
| 653 | assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); | ||
| 654 | assert_eq!(depot.link_find("d1/f2").unwrap(), None); | ||
| 655 | depot.link_remove(f4); | ||
| 656 | assert_eq!(depot.link_find("d1/d2/f4").unwrap(), None); | ||
| 657 | depot.link_remove(d3); | ||
| 658 | assert_eq!(depot.link_find("d3").unwrap(), None); | ||
| 659 | } | ||
| 660 | |||
| 661 | #[test] | ||
| 662 | fn test_depot_link_move() { | ||
| 663 | let mut depot = Depot::default(); | ||
| 664 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 665 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 666 | |||
| 667 | depot.link_move(f1, "").unwrap_err(); | ||
| 668 | depot.link_move(f1, "d1/f2/f1").unwrap_err(); | ||
| 669 | depot.link_move(f1, "d1/f2").unwrap_err(); | ||
| 670 | |||
| 671 | depot.link_move(f1, "f1").unwrap(); | ||
| 672 | assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); | ||
| 673 | depot.link_move(f1, "f2").unwrap(); | ||
| 674 | assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); | ||
| 675 | assert_eq!(depot.link_find("f2").unwrap(), Some(f1)); | ||
| 676 | } | ||
| 677 | |||
| 678 | #[test] | ||
| 679 | fn test_depot_link_search() { | ||
| 680 | let mut depot = Depot::default(); | ||
| 681 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 682 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 683 | let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 684 | let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 685 | let _d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 686 | |||
| 687 | assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); | ||
| 688 | assert_eq!( | ||
| 689 | depot.link_search("d1/f1/f5").unwrap(), | ||
| 690 | SearchResult::Ancestor(f1) | ||
| 691 | ); | ||
| 692 | assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound); | ||
| 693 | assert_eq!( | ||
| 694 | depot.link_search("d1/d2/f5").unwrap(), | ||
| 695 | SearchResult::NotFound | ||
| 696 | ); | ||
| 697 | } | ||
| 698 | |||
| 699 | #[test] | ||
| 700 | fn test_depot_link_find() { | ||
| 701 | let mut depot = Depot::default(); | ||
| 702 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 703 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 704 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 705 | let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 706 | let d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 707 | |||
| 708 | assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); | ||
| 709 | assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); | ||
| 710 | assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); | ||
| 711 | assert_eq!(depot.link_find("d3").unwrap(), Some(d3)); | ||
| 712 | |||
| 713 | assert_eq!(depot.link_find("d5").unwrap(), None); | ||
| 714 | assert_eq!(depot.link_find("d3/d5").unwrap(), None); | ||
| 715 | assert_eq!(depot.link_find("d1/d2/f5").unwrap(), None); | ||
| 716 | } | ||
| 717 | |||
| 718 | #[test] | ||
| 719 | fn test_depot_links_under() { | ||
| 720 | let mut depot = Depot::default(); | ||
| 721 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 722 | let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 723 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 724 | let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 725 | let d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 726 | |||
| 727 | let under_f1 = depot.links_under("d1/f1").unwrap().collect::<Vec<_>>(); | ||
| 728 | assert_eq!(under_f1, vec![f1]); | ||
| 729 | |||
| 730 | let under_d1 = depot.links_under("d1").unwrap().collect::<Vec<_>>(); | ||
| 731 | let expected_under_d1 = vec![f1, f2, f3, f4]; | ||
| 732 | assert!( | ||
| 733 | under_d1.len() == expected_under_d1.len() | ||
| 734 | && expected_under_d1.iter().all(|x| under_d1.contains(x)) | ||
| 735 | ); | ||
| 736 | |||
| 737 | let under_d2 = depot.links_under("d2").unwrap().collect::<Vec<_>>(); | ||
| 738 | assert_eq!(under_d2, vec![]); | ||
| 739 | |||
| 740 | let under_d3 = depot.links_under("d3").unwrap().collect::<Vec<_>>(); | ||
| 741 | assert_eq!(under_d3, vec![d3]); | ||
| 742 | |||
| 743 | let under_root = depot.links_under("").unwrap().collect::<Vec<_>>(); | ||
| 744 | let expected_under_root = vec![f1, f2, f3, f4, d3]; | ||
| 745 | assert!( | ||
| 746 | under_root.len() == expected_under_root.len() | ||
| 747 | && expected_under_root.iter().all(|x| under_root.contains(x)) | ||
| 748 | ); | ||
| 749 | } | ||
| 750 | |||
| 751 | #[test] | ||
| 752 | fn test_depot_has_links_under() { | ||
| 753 | let mut depot = Depot::default(); | ||
| 754 | let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 755 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 756 | let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 757 | let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 758 | let _d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 759 | |||
| 760 | assert!(depot.has_links_under("").unwrap()); | ||
| 761 | assert!(depot.has_links_under("d1").unwrap()); | ||
| 762 | assert!(depot.has_links_under("d3").unwrap()); | ||
| 763 | assert!(depot.has_links_under("d1/f1").unwrap()); | ||
| 764 | assert!(depot.has_links_under("d1/d2").unwrap()); | ||
| 765 | assert!(depot.has_links_under("d1/d2/f4").unwrap()); | ||
| 766 | |||
| 767 | assert!(!depot.has_links_under("d2").unwrap()); | ||
| 768 | assert!(!depot.has_links_under("d4").unwrap()); | ||
| 769 | assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); | ||
| 770 | } | ||
| 771 | |||
| 772 | #[test] | ||
| 773 | fn test_depot_links_verify_install() { | ||
| 774 | let mut depot = Depot::default(); | ||
| 775 | let f1 = depot.link_create("nvim", ".config/nvim").unwrap(); | ||
| 776 | let f2 = depot.link_create("alacritty", ".config/alacritty").unwrap(); | ||
| 777 | let f3 = depot.link_create("bash/.bashrc", ".bashrc").unwrap(); | ||
| 778 | let f4 = depot.link_create("bash_laptop/.bashrc", ".bashrc").unwrap(); | ||
| 779 | |||
| 780 | depot | ||
| 781 | .links_verify_install(vec![f1, f2, f3].into_iter()) | ||
| 782 | .unwrap(); | ||
| 783 | depot | ||
| 784 | .links_verify_install(vec![f1, f2, f3, f4].into_iter()) | ||
| 785 | .unwrap_err(); | ||
| 786 | } | ||
| 787 | |||
| 788 | #[test] | ||
| 789 | fn test_depot_read_dir() { | ||
| 790 | let mut depot = Depot::default(); | ||
| 791 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 792 | let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 793 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 794 | let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 795 | let _d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 796 | |||
| 797 | let read_dir = depot.read_dir("d1").unwrap().collect::<Vec<_>>(); | ||
| 798 | let expected_read_dir = vec![ | ||
| 799 | DirNode::Link(f1), | ||
| 800 | DirNode::Link(f2), | ||
| 801 | DirNode::Link(f3), | ||
| 802 | DirNode::Directory(PathBuf::from("d1/d2")), | ||
| 803 | ]; | ||
| 804 | assert!( | ||
| 805 | read_dir.len() == expected_read_dir.len() | ||
| 806 | && expected_read_dir.iter().all(|x| read_dir.contains(x)) | ||
| 807 | ); | ||
| 808 | } | ||
| 809 | |||
| 810 | #[test] | ||
| 811 | fn test_path_verify() { | ||
| 812 | path_verify(Path::new("")).unwrap(); | ||
| 813 | path_verify(Path::new("f1")).unwrap(); | ||
| 814 | path_verify(Path::new("d1/f1")).unwrap(); | ||
| 815 | path_verify(Path::new("d1/f1.txt")).unwrap(); | ||
| 816 | path_verify(Path::new("d1/./f1.txt")).unwrap(); | ||
| 817 | |||
| 818 | path_verify(Path::new("/")).unwrap_err(); | ||
| 819 | path_verify(Path::new("./f1")).unwrap_err(); | ||
| 820 | path_verify(Path::new("/d1/f1")).unwrap_err(); | ||
| 821 | path_verify(Path::new("d1/../f1.txt")).unwrap_err(); | ||
| 822 | path_verify(Path::new("/d1/../f1.txt")).unwrap_err(); | ||
| 823 | } | ||
| 824 | |||
| 825 | #[test] | ||
| 826 | fn test_path_verify_link() { | ||
| 827 | path_verify_link(Path::new("f1")).unwrap(); | ||
| 828 | path_verify_link(Path::new("d1/f1")).unwrap(); | ||
| 829 | path_verify_link(Path::new("d1/f1.txt")).unwrap(); | ||
| 830 | path_verify_link(Path::new("d1/./f1.txt")).unwrap(); | ||
| 831 | |||
| 832 | path_verify_link(Path::new("")).unwrap_err(); | ||
| 833 | path_verify_link(Path::new("/")).unwrap_err(); | ||
| 834 | path_verify_link(Path::new("./f1")).unwrap_err(); | ||
| 835 | path_verify_link(Path::new("/d1/f1")).unwrap_err(); | ||
| 836 | path_verify_link(Path::new("d1/../f1.txt")).unwrap_err(); | ||
| 837 | path_verify_link(Path::new("/d1/../f1.txt")).unwrap_err(); | ||
| 838 | } | ||
| 839 | |||
| 840 | #[test] | ||
| 841 | fn test_path_iter_comps() { | ||
| 842 | let path = Path::new("comp1/comp2/./comp3/file.txt"); | ||
| 843 | let mut iter = path_iter_comps(path); | ||
| 844 | assert_eq!(iter.next(), Some(OsStr::new("comp1"))); | ||
| 845 | assert_eq!(iter.next(), Some(OsStr::new("comp2"))); | ||
| 846 | assert_eq!(iter.next(), Some(OsStr::new("comp3"))); | ||
| 847 | assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); | ||
| 848 | assert_eq!(iter.next(), None); | ||
| 849 | } | ||
| 850 | } | ||
