jannie

jannie (mirror)
Log | Files | Refs | README | LICENSE

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 }