lib.rs (38843B)
1 pub mod args { 2 use std::path::PathBuf; 3 4 #[derive(Debug)] 5 pub struct Args { 6 pub config_dir: PathBuf, 7 pub debug: bool, 8 pub version: bool, 9 } 10 11 pub fn parse_args() -> Result<Args, Error> { 12 let mut args = std::env::args().skip(1); 13 let mut config_dir = None; 14 let mut debug = None; 15 let mut version = None; 16 loop { 17 let Some(arg) = args.next() else { 18 break; 19 }; 20 match arg.as_str() { 21 "-d" | "--dir" => { 22 let Some(value) = args.next() else { 23 return Err(Error::NotEnoughArguments); 24 }; 25 config_dir = Some(value.into()); 26 } 27 "--debug" => { 28 debug = Some(true); 29 } 30 "-V" | "--version" => { 31 version = Some(true); 32 } 33 _ => { 34 return Err(Error::UnknownArgument); 35 } 36 } 37 } 38 Ok(Args { 39 config_dir: config_dir.unwrap_or_else(|| "~/.config/jannie".into()), 40 debug: debug.unwrap_or(false), 41 version: version.unwrap_or(false), 42 }) 43 } 44 45 #[derive(Debug)] 46 pub enum Error { 47 UnknownArgument, 48 NotEnoughArguments, 49 } 50 51 impl std::error::Error for Error { 52 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 53 None 54 } 55 } 56 57 impl std::fmt::Display for Error { 58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 59 match self { 60 Error::UnknownArgument => write!(f, "Unknown argument"), 61 Error::NotEnoughArguments => write!(f, "Not enough arguments"), 62 } 63 } 64 } 65 } 66 67 pub mod config { 68 use std::path::PathBuf; 69 70 #[derive(Clone, Debug)] 71 pub struct Config { 72 pub root: PathBuf, 73 pub filters: Vec<Filter>, 74 } 75 76 #[derive(Clone, Debug)] 77 pub struct Filter { 78 pub path: PathBuf, 79 pub filter_type: FilterType, 80 } 81 82 impl Filter { 83 pub fn is_whitelist(&self) -> bool { 84 self.filter_type.is_whitelist() 85 } 86 87 pub fn is_blacklist(&self) -> bool { 88 self.filter_type.is_blacklist() 89 } 90 } 91 92 #[derive(Clone, Debug, Default)] 93 pub enum FilterType { 94 #[default] 95 Whitelist, 96 Blacklist, 97 } 98 99 impl FilterType { 100 pub fn is_whitelist(&self) -> bool { 101 matches!(self, FilterType::Whitelist) 102 } 103 104 pub fn is_blacklist(&self) -> bool { 105 matches!(self, FilterType::Blacklist) 106 } 107 } 108 109 pub fn parse_config(file: &str) -> Result<Config, Error> { 110 let mut root = None; 111 let mut filters = Vec::new(); 112 let mut state = State::Global; 113 let mut lines = file.lines(); 114 loop { 115 let Some(line) = lines.next() else { 116 break; 117 }; 118 let line_trimmed = line.trim(); 119 if line_trimmed.starts_with("#") { 120 continue; 121 } 122 if line_trimmed.is_empty() { 123 continue; 124 } 125 match state { 126 State::Global => { 127 if line_trimmed == "filters:" { 128 state = State::Filters; 129 } else { 130 let Some((key, value)) = line_trimmed.split_once(": ") else { 131 return Err(Error::InvalidInput); 132 }; 133 if key == "root" { 134 if root.is_some() { 135 return Err(Error::RootDefinedTwice); 136 } 137 root = Some(value.into()); 138 } else { 139 return Err(Error::InvalidInput); 140 } 141 } 142 } 143 State::Filters => { 144 let Some((key, value)) = line_trimmed.split_once(": ") else { 145 return Err(Error::InvalidInput); 146 }; 147 match key { 148 "whitelist" => filters.push(Filter { 149 path: value.into(), 150 filter_type: FilterType::Whitelist, 151 }), 152 "blacklist" => filters.push(Filter { 153 path: value.into(), 154 filter_type: FilterType::Blacklist, 155 }), 156 _ => return Err(Error::InvalidInput), 157 }; 158 } 159 } 160 } 161 Ok(Config { 162 root: root.unwrap_or_else(|| "~".into()), 163 filters, 164 }) 165 } 166 167 enum State { 168 Global, 169 Filters, 170 } 171 172 #[derive(Debug)] 173 pub enum Error { 174 InvalidInput, 175 RootDefinedTwice, 176 } 177 178 impl std::error::Error for Error { 179 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 180 None 181 } 182 } 183 184 impl std::fmt::Display for Error { 185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 186 match self { 187 Error::InvalidInput => write!(f, "Invalid config input"), 188 Error::RootDefinedTwice => write!(f, "Root path defined twice"), 189 } 190 } 191 } 192 } 193 194 pub mod filetree { 195 pub mod node { 196 use std::path::{Path, PathBuf}; 197 /// Allows any additional data to be provided 198 pub struct Node<M> { 199 id: usize, 200 path: PathBuf, 201 /// Any additional data 202 meta: M, 203 children: Vec<usize>, 204 parent: Option<usize>, 205 } 206 207 impl<M> Node<M> { 208 pub fn new(path: PathBuf, id: usize, meta: M) -> Self { 209 Self { 210 id, 211 path, 212 meta, 213 children: Vec::new(), 214 parent: None, 215 } 216 } 217 218 pub fn id(&self) -> usize { 219 self.id 220 } 221 222 pub fn path(&self) -> &Path { 223 &self.path 224 } 225 226 pub fn meta(&self) -> &M { 227 &self.meta 228 } 229 230 pub fn meta_mut(&mut self) -> &mut M { 231 &mut self.meta 232 } 233 234 pub fn set_meta(&mut self, meta: M) { 235 self.meta = meta; 236 } 237 238 /// A node is a leaf if it has no children 239 pub fn is_leaf(&self) -> bool { 240 self.children.is_empty() 241 } 242 243 pub fn parent(&self) -> Option<usize> { 244 self.parent 245 } 246 247 pub fn children(&self) -> &[usize] { 248 &self.children 249 } 250 251 pub fn add_child(&mut self, node_id: usize) { 252 self.children.push(node_id); 253 } 254 255 pub fn set_parent_opt(&mut self, parent_id: Option<usize>) { 256 self.parent = parent_id; 257 } 258 259 pub fn set_parent(&mut self, parent_id: usize) { 260 self.parent = Some(parent_id); 261 } 262 263 pub fn unset_parent(&mut self) { 264 self.parent = None; 265 } 266 } 267 } 268 269 use std::{ 270 collections::BTreeMap, 271 path::{Path, PathBuf}, 272 }; 273 274 use node::Node; 275 276 /// The tree currently is insert-only, it's impossible to rearrange it or 277 /// remove nodes or alter its structure in other ways. 278 pub struct Filetree<M> { 279 /// Root is node 0 280 nodes: Vec<Node<M>>, 281 paths: BTreeMap<PathBuf, usize>, 282 } 283 284 impl<M> Filetree<M> { 285 pub fn new(root_path: impl Into<PathBuf>, root_meta: M) -> Self { 286 let root_path = root_path.into(); 287 let mut paths = BTreeMap::new(); 288 paths.insert(root_path.clone(), 0); 289 let root = Node::new(root_path, 0, root_meta); 290 Filetree { 291 nodes: vec![root], 292 paths, 293 } 294 } 295 296 pub fn root(&self) -> &Node<M> { 297 &self.nodes[0] 298 } 299 300 pub fn nodes(&self) -> &[Node<M>] { 301 &self.nodes 302 } 303 304 pub fn get_node(&self, node_id: usize) -> Option<&Node<M>> { 305 self.nodes.get(node_id) 306 } 307 308 fn get_node_mut(&mut self, node_id: usize) -> Option<&mut Node<M>> { 309 self.nodes.get_mut(node_id) 310 } 311 312 pub fn get_node_by_path(&self, path: &Path) -> Option<&Node<M>> { 313 self.paths 314 .get(path) 315 .copied() 316 .map(|node_id| &self.nodes[node_id]) 317 } 318 319 pub fn get_node_id_by_path(&self, path: &Path) -> Option<usize> { 320 self.paths.get(path).copied() 321 } 322 323 pub fn get_meta(&self, node_id: usize) -> Option<&M> { 324 Some(self.get_node(node_id)?.meta()) 325 } 326 327 pub fn get_meta_mut(&mut self, node_id: usize) -> Option<&mut M> { 328 Some(self.get_node_mut(node_id)?.meta_mut()) 329 } 330 331 pub fn get_meta_by_path(&self, path: &Path) -> Option<&M> { 332 self.get_meta(self.get_node_id_by_path(path)?) 333 } 334 335 pub fn get_meta_mut_by_path(&mut self, path: &Path) -> Option<&mut M> { 336 self.get_meta_mut(self.get_node_id_by_path(path)?) 337 } 338 339 /// Checks if a node with this path exists 340 pub fn check_path(&self, path: &Path) -> bool { 341 self.paths.get(path).is_some() 342 } 343 344 pub fn insert(&mut self, path: impl Into<PathBuf>, meta: M) -> Result<(), Error> { 345 let path = path.into(); 346 log::debug!("Trying to add node with path {path:?}"); 347 if path.ends_with("..") { 348 return Err(Error::ParentDir); 349 } 350 if path.ends_with(".") { 351 return Err(Error::CurrentDir); 352 } 353 if self.check_path(&path) { 354 return Err(Error::NodeExists); 355 } 356 let Some(parent) = path.parent() else { 357 return Err(Error::MissingPath); 358 }; 359 log::trace!("Searching for node with path {parent:?}"); 360 if let Some(parent) = self.paths.get(parent).copied() { 361 let id = self.nodes.len(); 362 let mut node = Node::new(path.clone(), id, meta); 363 node.set_parent(parent); 364 self.nodes.push(node); 365 self.paths.insert(path, id); 366 self.nodes[parent].add_child(id); 367 Ok(()) 368 } else { 369 Err(Error::MissingPath) 370 } 371 } 372 } 373 374 impl<M> Filetree<M> { 375 pub fn print(&self) -> Result<(), Error> { 376 let mut print_buffer = Vec::new(); 377 print_buffer.push((0, 0)); 378 while let Some((offset, node_id)) = print_buffer.pop() { 379 let node = self.nodes.get(node_id).expect("Shold have the node"); 380 println!( 381 "{}| {}", 382 " ".repeat(offset), 383 node.path() 384 .file_name() 385 .map(|name| name.to_str()) 386 .flatten() 387 .unwrap_or("UNKNOWN") 388 ); 389 print_buffer.extend( 390 node.children() 391 .iter() 392 .copied() 393 .map(|child_id| (offset + 1, child_id)), 394 ); 395 } 396 Ok(()) 397 } 398 } 399 400 #[derive(Debug)] 401 pub enum Error { 402 NodeExists, 403 MissingPath, 404 CurrentDir, 405 ParentDir, 406 } 407 408 impl std::error::Error for Error { 409 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 410 None 411 } 412 } 413 414 impl std::fmt::Display for Error { 415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 416 match self { 417 Error::NodeExists => write!(f, "Node with such path already exists"), 418 Error::MissingPath => write!(f, "Parent path is missing"), 419 Error::CurrentDir => write!(f, "Used current dir (.) in path"), 420 Error::ParentDir => write!(f, "Used parent dir (..) in path"), 421 } 422 } 423 } 424 425 #[cfg(test)] 426 mod tests { 427 use super::*; 428 429 #[test] 430 fn cannot_insert_root() { 431 let mut tree = Filetree::new("/", ()); 432 assert!(dbg!(tree.insert("/", ())).is_err()); 433 } 434 435 #[test] 436 fn cannot_insert_relative() { 437 let mut tree = Filetree::new("/", ()); 438 assert!(dbg!(tree.insert("dir", ())).is_err()); 439 } 440 441 #[test] 442 fn cannot_insert_dots() { 443 let mut tree = Filetree::new("/", ()); 444 assert!(dbg!(tree.insert("/.", ())).is_err()); 445 assert!(dbg!(tree.insert("/..", ())).is_err()); 446 } 447 448 #[test] 449 fn insert_one() { 450 let mut tree = Filetree::new("/", ()); 451 assert!(dbg!(tree.insert("/dir", ())).is_ok()); 452 } 453 454 #[test] 455 fn insert_exists() { 456 let mut tree = Filetree::new("/", ()); 457 assert!(dbg!(tree.insert("/dir", ())).is_ok()); 458 assert!(dbg!(tree.insert("/dir", ())).is_err()); 459 } 460 461 #[test] 462 fn insert_many() { 463 let mut tree = Filetree::new("/", ()); 464 assert!(dbg!(tree.insert("/dir", ())).is_ok()); 465 assert!(dbg!(tree.insert("/dir/dir/dir", ())).is_err()); 466 assert!(dbg!(tree.insert("/dir/dir", ())).is_ok()); 467 assert!(dbg!(tree.insert("/dir/dir", ())).is_err()); 468 assert!(dbg!(tree.insert("/dir/dir/dir", ())).is_ok()); 469 assert!(dbg!(tree.insert("/dir2", ())).is_ok()); 470 assert!(dbg!(tree.insert("/dir3", ())).is_ok()); 471 } 472 } 473 } 474 475 pub mod inventory { 476 use std::path::PathBuf; 477 478 #[derive(Debug)] 479 pub struct Inventory { 480 pub items: Vec<Item>, 481 } 482 483 #[derive(Debug)] 484 pub struct Item { 485 pub path: PathBuf, 486 pub check: Check, 487 } 488 489 #[derive(Clone, Debug, Default)] 490 pub enum Check { 491 #[default] 492 None, 493 Shell(String), 494 } 495 496 pub fn parse_inventory(file: &str) -> Result<Inventory, Error> { 497 let mut items = Vec::new(); 498 let mut state = State::Global; 499 let mut lines = file.lines(); 500 loop { 501 let Some(line) = lines.next() else { 502 match state { 503 State::Global => break, 504 State::Item { path, check } => { 505 items.push(Item { 506 path, 507 check: check.unwrap_or_default(), 508 }); 509 break; 510 } 511 }; 512 }; 513 let line_trimmed = line.trim(); 514 if line_trimmed.starts_with("#") { 515 continue; 516 } 517 if line_trimmed.is_empty() { 518 continue; 519 } 520 match state { 521 State::Global => { 522 let Some((key, value)) = line_trimmed.split_once(": ") else { 523 return Err(Error::InvalidInput); 524 }; 525 if key == "path" { 526 state = State::Item { 527 path: value.into(), 528 check: None, 529 }; 530 } else { 531 return Err(Error::InvalidInput); 532 } 533 } 534 State::Item { path, check } => { 535 let Some((key, value)) = line_trimmed.split_once(": ") else { 536 return Err(Error::InvalidInput); 537 }; 538 match key { 539 "path" => { 540 items.push(Item { 541 path, 542 check: check.unwrap_or_default(), 543 }); 544 state = State::Item { 545 path: value.into(), 546 check: None, 547 }; 548 } 549 "check" => { 550 if check.is_some() { 551 return Err(Error::CheckDefinedTwice); 552 } 553 let check = if value == "none" { 554 Check::None 555 } else if let Some(("shell", command)) = value.split_once(" ") { 556 Check::Shell(command.into()) 557 } else { 558 return Err(Error::InvalidInput); 559 }; 560 state = State::Item { 561 path, 562 check: Some(check), 563 }; 564 } 565 _ => return Err(Error::InvalidInput), 566 }; 567 } 568 } 569 } 570 Ok(Inventory { items }) 571 } 572 573 enum State { 574 Global, 575 Item { path: PathBuf, check: Option<Check> }, 576 } 577 578 #[derive(Debug)] 579 pub enum Error { 580 InvalidInput, 581 CheckDefinedTwice, 582 } 583 584 impl std::error::Error for Error { 585 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 586 None 587 } 588 } 589 590 impl std::fmt::Display for Error { 591 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 592 match self { 593 Error::InvalidInput => write!(f, "Invalid input"), 594 Error::CheckDefinedTwice => write!(f, "Check field defined twice"), 595 } 596 } 597 } 598 } 599 600 pub mod logging { 601 //! Logging implementation. 602 //! Currently only logs to stderr. 603 //! 604 //! see [`setup_logging`] 605 606 use log::LevelFilter; 607 use std::sync::OnceLock; 608 609 static LOGGER: OnceLock<Logger> = OnceLock::new(); 610 611 pub struct Logger { 612 level: LevelFilter, 613 } 614 615 impl log::Log for Logger { 616 fn enabled(&self, metadata: &log::Metadata) -> bool { 617 metadata.level() <= self.level 618 } 619 620 fn log(&self, record: &log::Record) { 621 if self.enabled(record.metadata()) { 622 eprintln!("[{}]: {}", record.level(), record.args()); 623 } 624 } 625 626 fn flush(&self) {} 627 } 628 629 /// Sets up global logger with a given level. 630 /// 631 /// Note: this function may only be called once. 632 pub fn setup_logging(level: LevelFilter) { 633 assert!( 634 LOGGER.set(Logger { level }).is_ok(), 635 "Should initialize logger" 636 ); 637 log::set_logger(LOGGER.get().expect("Logger should be initialized")) 638 .expect("Should set logger"); 639 log::set_max_level(level); 640 } 641 } 642 643 pub mod path_processing { 644 use std::{ 645 env::VarError, 646 path::{Path, PathBuf}, 647 }; 648 649 pub fn expand_path(path: &Path) -> Result<PathBuf, Error> { 650 let path = shellexpand::full( 651 path.to_str() 652 .ok_or_else(|| Error::Parse(path.to_path_buf()))?, 653 ) 654 .map_err(Error::Expand)?; 655 Ok(path.as_ref().into()) 656 } 657 658 pub fn canonicalize_path(path: &Path) -> Result<PathBuf, Error> { 659 let path = path.canonicalize().map_err(Error::Canonicalize)?; 660 Ok(path) 661 } 662 663 /// Expands given path and canonicalizes it 664 pub fn process_path(path: &Path) -> Result<PathBuf, Error> { 665 let path = expand_path(path)?; 666 canonicalize_path(&path) 667 } 668 669 #[derive(Debug)] 670 pub enum Error { 671 Parse(PathBuf), 672 Expand(shellexpand::LookupError<VarError>), 673 Canonicalize(std::io::Error), 674 } 675 676 impl Error { 677 pub fn is_file_not_found(&self) -> bool { 678 match *self { 679 Error::Canonicalize(ref error) => match error.kind() { 680 std::io::ErrorKind::NotFound => true, 681 _ => false, 682 }, 683 _ => false, 684 } 685 } 686 } 687 688 impl std::error::Error for Error { 689 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 690 match self { 691 Error::Parse(_path) => None, 692 Error::Expand(error) => Some(error), 693 Error::Canonicalize(error) => Some(error), 694 } 695 } 696 } 697 698 impl std::fmt::Display for Error { 699 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 700 match self { 701 Error::Parse(path) => write!(f, "Error parsing path {path:?}"), 702 Error::Expand(error) => write!(f, "Error expanding path: {error}"), 703 Error::Canonicalize(error) => write!(f, "Error canonicalizing path: {error}"), 704 } 705 } 706 } 707 } 708 709 pub mod state { 710 use std::path::{Path, PathBuf}; 711 712 use super::config::{Config, Filter, FilterType}; 713 use super::filetree::{self, Filetree}; 714 use super::inventory::{self, Inventory}; 715 use super::path_processing; 716 717 pub struct State { 718 pub config_dir: PathBuf, 719 pub config: Config, 720 pub inventory: Inventory, 721 /// Paths in filters are absolute and processed 722 pub filters: Vec<Filter>, 723 /// Absolute and processed 724 pub root: PathBuf, 725 pub blacklist_tree: Filetree<()>, 726 pub whitelist_tree: Filetree<()>, 727 pub inventory_tree: Filetree<Option<inventory::Check>>, 728 } 729 730 impl State { 731 /// Parses inventory, builds whitelist and blacklist trees, builds an 732 /// inventory tree, normalizes root path and filters. 733 pub fn new(config_dir: impl Into<PathBuf>, config: Config) -> Result<Self, Error> { 734 let config_dir = config_dir.into(); 735 736 let inventory_path = config_dir.join("inventory.txt"); 737 log::trace!("Reading inventory from {inventory_path:?}"); 738 let inventory = 739 std::fs::read_to_string(inventory_path).map_err(Error::ReadInventoryFile)?; 740 log::trace!("Parsing inventory file contents"); 741 let inventory = 742 inventory::parse_inventory(&inventory).map_err(Error::ParseInventory)?; 743 744 log::trace!("Processing root path"); 745 let root = 746 path_processing::process_path(&config.root).map_err(Error::ProcessRootPath)?; 747 748 let mut filters = Vec::new(); 749 750 log::debug!("Preprocessing filters"); 751 752 for filter in &config.filters { 753 log::trace!("Preprocessing filter {filter:?}"); 754 let mut path = config.root.clone(); 755 path.push(&filter.path); 756 filters.push(Filter { 757 path: match path_processing::process_path(&path) { 758 Ok(path) => path, 759 Err(error) if error.is_file_not_found() => { 760 log::warn!("Filter path was not found: {filter:?}"); 761 continue; 762 } 763 Err(error) => Err(Error::ProcessPath(error))?, 764 }, 765 filter_type: filter.filter_type.clone(), 766 }); 767 } 768 769 log::debug!("Building trees from filters"); 770 771 let mut blacklist_tree = Filetree::new("/", ()); 772 let mut whitelist_tree = Filetree::new("/", ()); 773 774 for filter in &filters { 775 let path = &filter.path; 776 match filter.filter_type { 777 FilterType::Whitelist => { 778 log::debug!("Adding {:?} to whitelist", path); 779 add_to_tree(&mut whitelist_tree, &path)? 780 } 781 FilterType::Blacklist => { 782 if filters 783 .iter() 784 .filter(|filter| filter.is_whitelist()) 785 .find(|filter| &filter.path == path) 786 .is_some() 787 { 788 log::debug!("Skipping {:?} blacklist", path); 789 } else { 790 log::debug!("Adding {:?} to blacklist", path); 791 add_to_tree(&mut blacklist_tree, &path)? 792 } 793 } 794 } 795 } 796 797 log::debug!("Building inventory tree"); 798 799 let mut inventory_tree = Filetree::new("/", None); 800 801 for item in &inventory.items { 802 let path = match path_processing::process_path(&item.path) { 803 Ok(path) => path, 804 Err(error) if error.is_file_not_found() => { 805 log::warn!("Inventory item was not found: {item:?}"); 806 continue; 807 } 808 Err(error) => Err(Error::ProcessPath(error))?, 809 }; 810 let mut components = path.components(); 811 let mut path_buf = PathBuf::new(); 812 loop { 813 let component = components.next(); 814 match component { 815 Some(component) => { 816 path_buf.push(component); 817 if inventory_tree.get_node_by_path(&path_buf).is_none() { 818 inventory_tree 819 .insert(path_buf.clone(), None) 820 .map_err(Error::Filetree)?; 821 } 822 } 823 None => { 824 *inventory_tree 825 .get_meta_mut_by_path(&path) 826 .expect("Should have added inventory item") = 827 Some(item.check.clone()); 828 break; 829 } 830 } 831 } 832 } 833 834 Ok(Self { 835 config_dir: config_dir.into(), 836 config, 837 inventory, 838 filters, 839 root, 840 blacklist_tree, 841 whitelist_tree, 842 inventory_tree, 843 }) 844 } 845 } 846 847 fn add_to_tree(tree: &mut Filetree<()>, path: &Path) -> Result<(), Error> { 848 let mut components = path.components(); 849 let mut path_buf = PathBuf::new(); 850 loop { 851 let component = components.next(); 852 match component { 853 Some(component) => { 854 path_buf.push(component); 855 if tree.get_node_by_path(&path_buf).is_none() { 856 tree.insert(path_buf.clone(), ()).map_err(Error::Filetree)?; 857 } 858 } 859 None => { 860 assert!(tree.get_node_by_path(path).is_some()); 861 break; 862 } 863 } 864 } 865 Ok(()) 866 } 867 868 #[derive(Debug)] 869 pub enum Error { 870 ReadInventoryFile(std::io::Error), 871 ProcessPath(path_processing::Error), 872 ProcessRootPath(path_processing::Error), 873 ParseInventory(inventory::Error), 874 Filetree(filetree::Error), 875 } 876 877 impl std::error::Error for Error { 878 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 879 match self { 880 Error::ReadInventoryFile(error) => Some(error), 881 Error::ProcessPath(error) => Some(error), 882 Error::ProcessRootPath(error) => Some(error), 883 Error::ParseInventory(error) => Some(error), 884 Error::Filetree(error) => Some(error), 885 } 886 } 887 } 888 889 impl std::fmt::Display for Error { 890 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 891 match self { 892 Error::ReadInventoryFile(error) => { 893 write!(f, "Error reading inventory file: {error}") 894 } 895 Error::ProcessPath(error) => write!(f, "Error processing path: {error}"), 896 Error::ProcessRootPath(error) => write!(f, "Error processing root path: {error}"), 897 Error::ParseInventory(error) => write!(f, "Error parsing inventory: {error}"), 898 Error::Filetree(error) => write!(f, "Filetree error: {error}"), 899 } 900 } 901 } 902 } 903 904 use std::{collections::VecDeque, path::Path}; 905 906 use filetree::Filetree; 907 use state::State; 908 909 pub fn oneshot() -> Result<(), Box<dyn std::error::Error>> { 910 let args = args::parse_args().map_err(Error::ParseArgs)?; 911 912 if args.version { 913 println!("jannie {}", env!("CARGO_PKG_VERSION")); 914 return Ok(()); 915 } 916 917 let config_dir = 918 path_processing::process_path(&args.config_dir).map_err(Error::ProcessDirPath)?; 919 let config_path = config_dir.join("config.txt"); 920 let config_file = std::fs::read_to_string(&config_path).map_err(Error::ReadConfigFile)?; 921 let config = config::parse_config(&config_file).map_err(Error::ParseConfig)?; 922 923 logging::setup_logging(if args.debug { 924 log::LevelFilter::Trace 925 } else { 926 log::LevelFilter::Warn 927 }); 928 929 log::debug!("Starting with args: {args:#?}"); 930 log::debug!("Read config: {config:#?}"); 931 932 let state = State::new(&config_dir, config)?; 933 934 log::debug!("Read inventory: {:#?}", state.inventory); 935 936 read(&state)?; 937 938 Ok(()) 939 } 940 941 /// This meta is used for display tree. 942 #[derive(Clone, Debug)] 943 struct Meta { 944 inventory: Option<Option<String>>, 945 blacklist: Option<bool>, 946 } 947 948 /// Scan workspace and display its tree. 949 fn read(state: &State) -> Result<(), Error> { 950 let mut filetree = Filetree::new( 951 "/", 952 Meta { 953 blacklist: None, 954 inventory: None, 955 }, 956 ); 957 958 let mut processing_buffer = Vec::new(); 959 960 // Add all whitelist to buffer 961 for filter in state.filters.iter().filter(|filter| filter.is_whitelist()) { 962 processing_buffer.push(filter.path.clone()); 963 } 964 965 // This makes it so every prefix goes first. 966 processing_buffer.sort(); 967 968 // Has some specific ordering so the tree is built in the correct order. 969 let mut processing_buffer = VecDeque::from(processing_buffer); 970 971 while let Some(item) = processing_buffer.pop_front() { 972 log::debug!("Adding {:?} to tree", item); 973 974 let allowed_to_scan = allowed_to_scan(state, &item)?; 975 let inventory_message = (|| -> Result<Option<Option<String>>, Error> { 976 let Some(node) = state.inventory_tree.get_node_by_path(&item) else { 977 return Ok(None); 978 }; 979 if !node.is_leaf() { 980 return Ok(None); 981 } 982 let Some(check) = node.meta().clone() else { 983 return Err(Error::NoInventoryCheckInfo); 984 }; 985 Ok(Some(match check { 986 inventory::Check::None => None, 987 inventory::Check::Shell(cmd) => Some( 988 String::from_utf8( 989 std::process::Command::new("sh") 990 .arg("-c") 991 .arg(cmd) 992 .output() 993 .map_err(Error::Command)? 994 .stdout, 995 ) 996 .map_err(Error::StringUtf8)?, 997 ), 998 })) 999 })()?; 1000 1001 if let Some(meta) = filetree.get_meta_mut_by_path(&item) { 1002 meta.blacklist = Some(!allowed_to_scan); 1003 } else { 1004 // The earliest prefix that already added 1005 let mut base = item.clone(); 1006 while !filetree.check_path(&base) { 1007 base.pop(); 1008 } 1009 1010 // What is not yet added 1011 let mut suffix = item 1012 .strip_prefix(&base) 1013 .map_err(Error::BasePrefix)? 1014 .to_owned(); 1015 suffix.pop(); 1016 1017 for component in suffix.components() { 1018 base.push(component); 1019 filetree 1020 .insert( 1021 base.clone(), 1022 Meta { 1023 blacklist: None, 1024 inventory: None, 1025 }, 1026 ) 1027 .map_err(Error::Filetree)?; 1028 } 1029 filetree 1030 .insert( 1031 item.clone(), 1032 Meta { 1033 blacklist: Some(!allowed_to_scan), 1034 inventory: inventory_message, 1035 }, 1036 ) 1037 .map_err(Error::Filetree)?; 1038 } 1039 1040 if allowed_to_scan && should_scan(state, &item) { 1041 log::debug!("Scanning {:?}", item.file_name()); 1042 let mut iter = std::fs::read_dir(&item).map_err(Error::ReadFs)?; 1043 1044 let mut dir_buffer = Vec::new(); 1045 1046 while let Some(result) = iter.next().transpose().map_err(Error::ReadFs)? { 1047 let path = result.path(); 1048 dir_buffer.push(path) 1049 } 1050 1051 dir_buffer.sort(); 1052 1053 while let Some(path) = dir_buffer.pop() { 1054 processing_buffer.push_front(path); 1055 } 1056 } 1057 } 1058 1059 print(&filetree)?; 1060 1061 Ok(()) 1062 } 1063 1064 /// Does not check if allowed. 1065 /// Please check if allowed before doing this. 1066 fn should_scan(state: &State, path: &Path) -> bool { 1067 // is allowed 1068 // and is a prefix of a blacklist that is not overridden or is a prefix of an inventory item 1069 let inventory = if let Some(node) = state.inventory_tree.get_node_by_path(path) { 1070 !node.is_leaf() 1071 } else { 1072 false 1073 }; 1074 1075 state.blacklist_tree.check_path(path) || inventory 1076 } 1077 1078 /// If a directory is allowed to be scanned. 1079 /// Whitelist has more priority than blacklist on the same level of specificity. 1080 fn allowed_to_scan(state: &State, path: &Path) -> Result<bool, Error> { 1081 // If there is no not-overridden blacklist 1082 1083 // If no such blacklist filter found that blacklists the path such that 1084 // for this blacklist filter no such whitelist found that whitelists the 1085 // path back aka for which the blacklist is the prefix 1086 Ok(!state 1087 .filters 1088 .iter() 1089 .filter(|filter| filter.is_blacklist()) 1090 .any(|bl_filter| { 1091 path.starts_with(&bl_filter.path) 1092 && !state 1093 .filters 1094 .iter() 1095 .filter(|filter| filter.is_whitelist()) 1096 .any(|wl_filter| { 1097 path.starts_with(&wl_filter.path) 1098 && wl_filter.path.starts_with(&bl_filter.path) 1099 }) 1100 })) 1101 } 1102 1103 fn print(tree: &Filetree<Meta>) -> Result<(), Error> { 1104 let mut print_buffer = VecDeque::new(); 1105 println!( 1106 "{}", 1107 tree.root().path().to_str().unwrap_or("UNKNOWN").to_owned() 1108 ); 1109 print_buffer.extend( 1110 tree.root() 1111 .children() 1112 .iter() 1113 .copied() 1114 .map(|child_id| (1, child_id)), 1115 ); 1116 while let Some((offset, node_id)) = print_buffer.pop_front() { 1117 let node = tree.get_node(node_id).expect("Shold have the node"); 1118 let name = node 1119 .path() 1120 .file_name() 1121 .map(|name| name.to_str()) 1122 .flatten() 1123 .unwrap_or("UNKNOWN") 1124 .to_owned(); 1125 let offset_text = " ".repeat(offset); 1126 if let Some(msg) = node.meta().inventory.as_ref() { 1127 let msg = msg.as_deref().unwrap_or("\x1b[31mno info\x1b[0m"); 1128 println!( 1129 "{}\x1b[2m|\x1b[0m \x1b[32m{}\x1b[0m \x1b[3m{}\x1b[0m", 1130 offset_text, name, msg, 1131 ); 1132 } else if let Some(true) = node.meta().blacklist { 1133 println!("{}\x1b[2m|\x1b[0m \x1b[2;9m{}\x1b[0m", offset_text, name); 1134 } else if node.is_leaf() { 1135 println!("{}\x1b[2m|\x1b[0m \x1b[31m{}\x1b[0m", offset_text, name); 1136 } else { 1137 println!("{}\x1b[2m|\x1b[0m {}", offset_text, name); 1138 } 1139 for child in node 1140 .children() 1141 .iter() 1142 .copied() 1143 .map(|child_id| (offset + 1, child_id)) 1144 .collect::<Vec<_>>() 1145 .into_iter() 1146 .rev() 1147 { 1148 print_buffer.push_front(child); 1149 } 1150 } 1151 Ok(()) 1152 } 1153 1154 #[derive(Debug)] 1155 pub enum Error { 1156 ParseArgs(args::Error), 1157 ProcessDirPath(path_processing::Error), 1158 ReadConfigFile(std::io::Error), 1159 ParseConfig(config::Error), 1160 Filetree(filetree::Error), 1161 BasePrefix(std::path::StripPrefixError), 1162 NoInventoryCheckInfo, 1163 Command(std::io::Error), 1164 StringUtf8(std::string::FromUtf8Error), 1165 ReadFs(std::io::Error), 1166 } 1167 1168 impl std::error::Error for Error { 1169 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 1170 match self { 1171 Error::ParseArgs(error) => Some(error), 1172 Error::ProcessDirPath(error) => Some(error), 1173 Error::ReadConfigFile(error) => Some(error), 1174 Error::ParseConfig(error) => Some(error), 1175 Error::Filetree(error) => Some(error), 1176 Error::BasePrefix(error) => Some(error), 1177 Error::NoInventoryCheckInfo => None, 1178 Error::Command(error) => Some(error), 1179 Error::StringUtf8(error) => Some(error), 1180 Error::ReadFs(error) => Some(error), 1181 } 1182 } 1183 } 1184 1185 impl std::fmt::Display for Error { 1186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1187 match self { 1188 Error::ParseArgs(error) => write!(f, "Error parsing args: {error}"), 1189 Error::ProcessDirPath(error) => write!(f, "Error processing directory path: {error}"), 1190 Error::ReadConfigFile(error) => write!(f, "Error reading config file: {error}"), 1191 Error::ParseConfig(error) => write!(f, "Error parsing config: {error}"), 1192 Error::Filetree(error) => write!(f, "Filetree error: {error}"), 1193 Error::BasePrefix(error) => write!(f, "Error stripping prefix: {error}"), 1194 Error::NoInventoryCheckInfo => write!(f, "No inventory check info in filetree"), 1195 Error::Command(error) => write!(f, "Error executing command: {error}"), 1196 Error::StringUtf8(error) => write!(f, "Error building utf8 string: {error}"), 1197 Error::ReadFs(error) => write!(f, "Error reading from filesystem: {error}"), 1198 } 1199 } 1200 }