jannie

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

commit a33457a58df5ef8da428b277742e7306eee9a6ca
parent 2e85004b82cf51993a9771426f3a7a047a50e609
Author: Andy Khramtsov <>
Date:   Tue, 14 Jul 2026 01:34:03 +0300

feat: new implementation

Diffstat:
MCargo.lock | 7++++---
MCargo.toml | 3++-
Aconfig.txt | 9+++++++++
Ainventory.txt | 7+++++++
Asrc/jannie_new.rs | 1178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 1+
Msrc/main.rs | 2+-
7 files changed, 1202 insertions(+), 5 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -485,12 +485,13 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jannie" -version = "0.2.1" +version = "1.0.0-dev" dependencies = [ "clap", "config", "dialoguer", "indexmap", + "log", "serde", "serde_json", "serde_yaml", @@ -555,9 +556,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "matchers" diff --git a/Cargo.toml b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jannie" authors = ["Andy Khramtsov"] -version = "0.2.1" +version = "1.0.0-dev" edition = "2024" [dependencies] @@ -9,6 +9,7 @@ clap = { version = "4.5.54", features = ["derive"] } config = "0.15.19" dialoguer = "0.12.0" indexmap = "2.13.0" +log = "0.4.33" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" serde_yaml = "0.9.34" diff --git a/config.txt b/config.txt @@ -0,0 +1,9 @@ +root: ~ + +filters: +whitelist: my-space/projects/python +blacklist: my-space/projects/python/sound +whitelist: my-space/projects/rust +whitelist: my-space/projects/openscad +blacklist: my-space/projects/rust/jannie +whitelist: my-space/projects/rust/jannie/log diff --git a/inventory.txt b/inventory.txt @@ -0,0 +1,7 @@ +### +### Examples +### + +path: ~/.config/emacs +path: ~/.config/nvim +path: ~/dotfiles diff --git a/src/jannie_new.rs b/src/jannie_new.rs @@ -0,0 +1,1178 @@ +pub mod args { + use std::path::PathBuf; + + #[derive(Debug)] + pub struct Args { + pub config_dir: PathBuf, + } + + pub fn parse_args() -> Result<Args, Error> { + let mut args = std::env::args().skip(1); + let mut config_dir = None; + loop { + let Some(arg) = args.next() else { + break; + }; + match arg.as_str() { + "-d" | "--dir" => { + let Some(value) = args.next() else { + return Err(Error::NotEnoughArguments); + }; + config_dir = Some(value.into()); + } + _ => { + return Err(Error::UnknownArgument); + } + } + } + Ok(Args { + config_dir: config_dir.unwrap_or_else(|| "~/.config/jannie".into()), + }) + } + + #[derive(Debug)] + pub enum Error { + UnknownArgument, + NotEnoughArguments, + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UnknownArgument => write!(f, "Unknown argument"), + Error::NotEnoughArguments => write!(f, "Not enough arguments"), + } + } + } +} + +pub mod config { + use std::path::PathBuf; + + #[derive(Clone, Debug)] + pub struct Config { + pub root: PathBuf, + pub filters: Vec<Filter>, + } + + #[derive(Clone, Debug)] + pub struct Filter { + pub path: PathBuf, + pub filter_type: FilterType, + } + + impl Filter { + pub fn is_whitelist(&self) -> bool { + self.filter_type.is_whitelist() + } + + pub fn is_blacklist(&self) -> bool { + self.filter_type.is_blacklist() + } + } + + #[derive(Clone, Debug, Default)] + pub enum FilterType { + #[default] + Whitelist, + Blacklist, + } + + impl FilterType { + pub fn is_whitelist(&self) -> bool { + matches!(self, FilterType::Whitelist) + } + + pub fn is_blacklist(&self) -> bool { + matches!(self, FilterType::Blacklist) + } + } + + pub fn parse_config(file: &str) -> Result<Config, Error> { + let mut root = None; + let mut filters = Vec::new(); + let mut state = State::Global; + let mut lines = file.lines(); + loop { + let Some(line) = lines.next() else { + break; + }; + let line_trimmed = line.trim(); + if line_trimmed.starts_with("#") { + continue; + } + if line_trimmed.is_empty() { + continue; + } + match state { + State::Global => { + if line_trimmed == "filters:" { + state = State::Filters; + } else { + let Some((key, value)) = line_trimmed.split_once(": ") else { + return Err(Error::InvalidInput); + }; + if key == "root" { + if root.is_some() { + return Err(Error::RootDefinedTwice); + } + root = Some(value.into()); + } else { + return Err(Error::InvalidInput); + } + } + } + State::Filters => { + let Some((key, value)) = line_trimmed.split_once(": ") else { + return Err(Error::InvalidInput); + }; + match key { + "whitelist" => filters.push(Filter { + path: value.into(), + filter_type: FilterType::Whitelist, + }), + "blacklist" => filters.push(Filter { + path: value.into(), + filter_type: FilterType::Blacklist, + }), + _ => return Err(Error::InvalidInput), + }; + } + } + } + Ok(Config { + root: root.unwrap_or_else(|| "~".into()), + filters, + }) + } + + enum State { + Global, + Filters, + } + + #[derive(Debug)] + pub enum Error { + InvalidInput, + RootDefinedTwice, + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidInput => write!(f, "Invalid config input"), + Error::RootDefinedTwice => write!(f, "Root path defined twice"), + } + } + } +} + +pub mod filetree { + pub mod node { + use std::path::{Path, PathBuf}; + /// Allows any additional data to be provided + pub struct Node<M> { + id: usize, + path: PathBuf, + /// Any additional data + meta: M, + children: Vec<usize>, + parent: Option<usize>, + } + + impl<M> Node<M> { + pub fn new(path: PathBuf, id: usize, meta: M) -> Self { + Self { + id, + path, + meta, + children: Vec::new(), + parent: None, + } + } + + pub fn id(&self) -> usize { + self.id + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn meta(&self) -> &M { + &self.meta + } + + pub fn meta_mut(&mut self) -> &mut M { + &mut self.meta + } + + pub fn set_meta(&mut self, meta: M) { + self.meta = meta; + } + + /// A node is a leaf if it has no children + pub fn is_leaf(&self) -> bool { + self.children.is_empty() + } + + pub fn parent(&self) -> Option<usize> { + self.parent + } + + pub fn children(&self) -> &[usize] { + &self.children + } + + pub fn add_child(&mut self, node_id: usize) { + self.children.push(node_id); + } + + pub fn set_parent_opt(&mut self, parent_id: Option<usize>) { + self.parent = parent_id; + } + + pub fn set_parent(&mut self, parent_id: usize) { + self.parent = Some(parent_id); + } + + pub fn unset_parent(&mut self) { + self.parent = None; + } + } + } + + use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + }; + + use node::Node; + + /// The tree currently is insert-only, it's impossible to rearrange it or + /// remove nodes or alter its structure in other ways. + pub struct Filetree<M> { + /// Root is node 0 + nodes: Vec<Node<M>>, + paths: BTreeMap<PathBuf, usize>, + } + + impl<M> Filetree<M> { + pub fn new(root_path: impl Into<PathBuf>, root_meta: M) -> Self { + let root_path = root_path.into(); + let mut paths = BTreeMap::new(); + paths.insert(root_path.clone(), 0); + let root = Node::new(root_path, 0, root_meta); + Filetree { + nodes: vec![root], + paths, + } + } + + pub fn root(&self) -> &Node<M> { + &self.nodes[0] + } + + pub fn nodes(&self) -> &[Node<M>] { + &self.nodes + } + + pub fn get_node(&self, node_id: usize) -> Option<&Node<M>> { + self.nodes.get(node_id) + } + + fn get_node_mut(&mut self, node_id: usize) -> Option<&mut Node<M>> { + self.nodes.get_mut(node_id) + } + + pub fn get_node_by_path(&self, path: &Path) -> Option<&Node<M>> { + self.paths + .get(path) + .copied() + .map(|node_id| &self.nodes[node_id]) + } + + pub fn get_node_id_by_path(&self, path: &Path) -> Option<usize> { + self.paths.get(path).copied() + } + + pub fn get_meta(&self, node_id: usize) -> Option<&M> { + Some(self.get_node(node_id)?.meta()) + } + + pub fn get_meta_mut(&mut self, node_id: usize) -> Option<&mut M> { + Some(self.get_node_mut(node_id)?.meta_mut()) + } + + pub fn get_meta_by_path(&self, path: &Path) -> Option<&M> { + self.get_meta(self.get_node_id_by_path(path)?) + } + + pub fn get_meta_mut_by_path(&mut self, path: &Path) -> Option<&mut M> { + self.get_meta_mut(self.get_node_id_by_path(path)?) + } + + /// Checks if a node with this path exists + pub fn check_path(&self, path: &Path) -> bool { + self.paths.get(path).is_some() + } + + pub fn insert(&mut self, path: impl Into<PathBuf>, meta: M) -> Result<(), Error> { + let path = path.into(); + log::debug!("Trying to add node with path {path:?}"); + if path.ends_with("..") { + return Err(Error::ParentDir); + } + if path.ends_with(".") { + return Err(Error::CurrentDir); + } + if self.check_path(&path) { + return Err(Error::NodeExists); + } + let Some(parent) = path.parent() else { + return Err(Error::MissingPath); + }; + log::trace!("Searching for node with path {parent:?}"); + if let Some(parent) = self.paths.get(parent).copied() { + let id = self.nodes.len(); + let mut node = Node::new(path.clone(), id, meta); + node.set_parent(parent); + self.nodes.push(node); + self.paths.insert(path, id); + self.nodes[parent].add_child(id); + Ok(()) + } else { + Err(Error::MissingPath) + } + } + } + + impl<M> Filetree<M> { + pub fn print(&self) -> Result<(), Error> { + let mut print_buffer = Vec::new(); + print_buffer.push((0, 0)); + while let Some((offset, node_id)) = print_buffer.pop() { + let node = self.nodes.get(node_id).expect("Shold have the node"); + println!( + "{}| {}", + " ".repeat(offset), + node.path() + .file_name() + .map(|name| name.to_str()) + .flatten() + .unwrap_or("UNKNOWN") + ); + print_buffer.extend( + node.children() + .iter() + .copied() + .map(|child_id| (offset + 1, child_id)), + ); + } + Ok(()) + } + } + + #[derive(Debug)] + pub enum Error { + NodeExists, + MissingPath, + CurrentDir, + ParentDir, + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::NodeExists => write!(f, "Node with such path already exists"), + Error::MissingPath => write!(f, "Parent path is missing"), + Error::CurrentDir => write!(f, "Used current dir (.) in path"), + Error::ParentDir => write!(f, "Used parent dir (..) in path"), + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn cannot_insert_root() { + let mut tree = Filetree::new("/", ()); + assert!(dbg!(tree.insert("/", ())).is_err()); + } + + #[test] + fn cannot_insert_relative() { + let mut tree = Filetree::new("/", ()); + assert!(dbg!(tree.insert("dir", ())).is_err()); + } + + #[test] + fn cannot_insert_dots() { + let mut tree = Filetree::new("/", ()); + assert!(dbg!(tree.insert("/.", ())).is_err()); + assert!(dbg!(tree.insert("/..", ())).is_err()); + } + + #[test] + fn insert_one() { + let mut tree = Filetree::new("/", ()); + assert!(dbg!(tree.insert("/dir", ())).is_ok()); + } + + #[test] + fn insert_exists() { + let mut tree = Filetree::new("/", ()); + assert!(dbg!(tree.insert("/dir", ())).is_ok()); + assert!(dbg!(tree.insert("/dir", ())).is_err()); + } + + #[test] + fn insert_many() { + let mut tree = Filetree::new("/", ()); + assert!(dbg!(tree.insert("/dir", ())).is_ok()); + assert!(dbg!(tree.insert("/dir/dir/dir", ())).is_err()); + assert!(dbg!(tree.insert("/dir/dir", ())).is_ok()); + assert!(dbg!(tree.insert("/dir/dir", ())).is_err()); + assert!(dbg!(tree.insert("/dir/dir/dir", ())).is_ok()); + assert!(dbg!(tree.insert("/dir2", ())).is_ok()); + assert!(dbg!(tree.insert("/dir3", ())).is_ok()); + } + } +} + +pub mod inventory { + use std::path::PathBuf; + + #[derive(Debug)] + pub struct Inventory { + pub items: Vec<Item>, + } + + #[derive(Debug)] + pub struct Item { + pub path: PathBuf, + pub check: Check, + } + + #[derive(Clone, Debug, Default)] + pub enum Check { + #[default] + None, + Shell(String), + } + + pub fn parse_inventory(file: &str) -> Result<Inventory, Error> { + let mut items = Vec::new(); + let mut state = State::Global; + let mut lines = file.lines(); + loop { + let Some(line) = lines.next() else { + match state { + State::Global => break, + State::Item { path, check } => { + items.push(Item { + path, + check: check.unwrap_or_default(), + }); + break; + } + }; + }; + let line_trimmed = line.trim(); + if line_trimmed.starts_with("#") { + continue; + } + if line_trimmed.is_empty() { + continue; + } + match state { + State::Global => { + let Some((key, value)) = line_trimmed.split_once(": ") else { + return Err(Error::InvalidInput); + }; + if key == "path" { + state = State::Item { + path: value.into(), + check: None, + }; + } else { + return Err(Error::InvalidInput); + } + } + State::Item { path, check } => { + let Some((key, value)) = line_trimmed.split_once(": ") else { + return Err(Error::InvalidInput); + }; + match key { + "path" => { + items.push(Item { + path, + check: check.unwrap_or_default(), + }); + state = State::Item { + path: value.into(), + check: None, + }; + } + "check" => { + if check.is_some() { + return Err(Error::CheckDefinedTwice); + } + let check = if value == "none" { + Check::None + } else if let Some(("shell", command)) = value.split_once(" ") { + Check::Shell(command.into()) + } else { + return Err(Error::InvalidInput); + }; + state = State::Item { + path, + check: Some(check), + }; + } + _ => return Err(Error::InvalidInput), + }; + } + } + } + Ok(Inventory { items }) + } + + enum State { + Global, + Item { path: PathBuf, check: Option<Check> }, + } + + #[derive(Debug)] + pub enum Error { + InvalidInput, + CheckDefinedTwice, + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidInput => write!(f, "Invalid input"), + Error::CheckDefinedTwice => write!(f, "Check field defined twice"), + } + } + } +} + +pub mod logging { + //! Logging implementation. + //! Currently only logs to stderr. + //! + //! see [`setup_logging`] + + use log::LevelFilter; + use std::sync::OnceLock; + + static LOGGER: OnceLock<Logger> = OnceLock::new(); + + pub struct Logger { + level: LevelFilter, + } + + impl log::Log for Logger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= self.level + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + eprintln!("[{}]: {}", record.level(), record.args()); + } + } + + fn flush(&self) {} + } + + /// Sets up global logger with a given level. + /// + /// Note: this function may only be called once. + pub fn setup_logging(level: LevelFilter) { + assert!( + LOGGER.set(Logger { level }).is_ok(), + "Should initialize logger" + ); + log::set_logger(LOGGER.get().expect("Logger should be initialized")) + .expect("Should set logger"); + log::set_max_level(level); + } +} + +pub mod path_processing { + use std::{ + env::VarError, + path::{Path, PathBuf}, + }; + + pub fn expand_path(path: &Path) -> Result<PathBuf, Error> { + let path = shellexpand::full( + path.to_str() + .ok_or_else(|| Error::Parse(path.to_path_buf()))?, + ) + .map_err(Error::Expand)?; + Ok(path.as_ref().into()) + } + + pub fn canonicalize_path(path: &Path) -> Result<PathBuf, Error> { + let path = path.canonicalize().map_err(Error::Canonicalize)?; + Ok(path) + } + + /// Expands given path and canonicalizes it + pub fn process_path(path: &Path) -> Result<PathBuf, Error> { + let path = expand_path(path)?; + canonicalize_path(&path) + } + + #[derive(Debug)] + pub enum Error { + Parse(PathBuf), + Expand(shellexpand::LookupError<VarError>), + Canonicalize(std::io::Error), + } + + impl Error { + pub fn is_file_not_found(&self) -> bool { + match *self { + Error::Canonicalize(ref error) => match error.kind() { + std::io::ErrorKind::NotFound => true, + _ => false, + }, + _ => false, + } + } + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Parse(_path) => None, + Error::Expand(error) => Some(error), + Error::Canonicalize(error) => Some(error), + } + } + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Parse(path) => write!(f, "Error parsing path {path:?}"), + Error::Expand(error) => write!(f, "Error expanding path: {error}"), + Error::Canonicalize(error) => write!(f, "Error canonicalizing path: {error}"), + } + } + } +} + +pub mod state { + use std::path::{Path, PathBuf}; + + use super::config::{Config, Filter, FilterType}; + use super::filetree::{self, Filetree}; + use super::inventory::{self, Inventory}; + use super::path_processing; + + pub struct State { + pub config_dir: PathBuf, + pub config: Config, + pub inventory: Inventory, + /// Paths in filters are absolute and processed + pub filters: Vec<Filter>, + /// Absolute and processed + pub root: PathBuf, + pub blacklist_tree: Filetree<()>, + pub whitelist_tree: Filetree<()>, + pub inventory_tree: Filetree<Option<inventory::Check>>, + } + + impl State { + /// Parses inventory, builds whitelist and blacklist trees, builds an + /// inventory tree, normalizes root path and filters. + pub fn new(config_dir: impl Into<PathBuf>, config: Config) -> Result<Self, Error> { + let config_dir = config_dir.into(); + + let inventory_path = config_dir.join("inventory.txt"); + log::trace!("Reading inventory from {inventory_path:?}"); + let inventory = + std::fs::read_to_string(inventory_path).map_err(Error::ReadInventoryFile)?; + log::trace!("Parsing inventory file contents"); + let inventory = + inventory::parse_inventory(&inventory).map_err(Error::ParseInventory)?; + + log::trace!("Processing root path"); + let root = + path_processing::process_path(&config.root).map_err(Error::ProcessRootPath)?; + + let mut filters = Vec::new(); + + tracing::debug!("Preprocessing filters"); + + for filter in &config.filters { + log::trace!("Preprocessing filter {filter:?}"); + let mut path = config.root.clone(); + path.push(&filter.path); + filters.push(Filter { + path: match path_processing::process_path(&path) { + Ok(path) => path, + Err(error) if error.is_file_not_found() => { + log::warn!("Filter path was not found: {filter:?}"); + continue; + } + Err(error) => Err(Error::ProcessPath(error))?, + }, + filter_type: filter.filter_type.clone(), + }); + } + + log::debug!("Building trees from filters"); + + let mut blacklist_tree = Filetree::new("/", ()); + let mut whitelist_tree = Filetree::new("/", ()); + + for filter in &filters { + let path = &filter.path; + match filter.filter_type { + FilterType::Whitelist => { + log::debug!("Adding {:?} to whitelist", path); + add_to_tree(&mut whitelist_tree, &path)? + } + FilterType::Blacklist => { + if filters + .iter() + .filter(|filter| filter.is_whitelist()) + .find(|filter| &filter.path == path) + .is_some() + { + log::debug!("Skipping {:?} blacklist", path); + } else { + log::debug!("Adding {:?} to blacklist", path); + add_to_tree(&mut blacklist_tree, &path)? + } + } + } + } + + log::debug!("Building inventory tree"); + + let mut inventory_tree = Filetree::new("/", None); + + for item in &inventory.items { + let path = match path_processing::process_path(&item.path) { + Ok(path) => path, + Err(error) if error.is_file_not_found() => { + log::warn!("Inventory item was not found: {item:?}"); + continue; + } + Err(error) => Err(Error::ProcessPath(error))?, + }; + let mut components = path.components(); + let mut path_buf = PathBuf::new(); + loop { + let component = components.next(); + match component { + Some(component) => { + path_buf.push(component); + if inventory_tree.get_node_by_path(&path_buf).is_none() { + inventory_tree + .insert(path_buf.clone(), None) + .map_err(Error::Filetree)?; + } + } + None => { + *inventory_tree + .get_meta_mut_by_path(&path) + .expect("Should have added inventory item") = + Some(item.check.clone()); + break; + } + } + } + } + + Ok(Self { + config_dir: config_dir.into(), + config, + inventory, + filters, + root, + blacklist_tree, + whitelist_tree, + inventory_tree, + }) + } + } + + fn add_to_tree(tree: &mut Filetree<()>, path: &Path) -> Result<(), Error> { + let mut components = path.components(); + let mut path_buf = PathBuf::new(); + loop { + let component = components.next(); + match component { + Some(component) => { + path_buf.push(component); + if tree.get_node_by_path(&path_buf).is_none() { + tree.insert(path_buf.clone(), ()).map_err(Error::Filetree)?; + } + } + None => { + assert!(tree.get_node_by_path(path).is_some()); + break; + } + } + } + Ok(()) + } + + #[derive(Debug)] + pub enum Error { + ReadInventoryFile(std::io::Error), + ProcessPath(path_processing::Error), + ProcessRootPath(path_processing::Error), + ParseInventory(inventory::Error), + Filetree(filetree::Error), + } + + impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ReadInventoryFile(error) => Some(error), + Error::ProcessPath(error) => Some(error), + Error::ProcessRootPath(error) => Some(error), + Error::ParseInventory(error) => Some(error), + Error::Filetree(error) => Some(error), + } + } + } + + impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ReadInventoryFile(error) => { + write!(f, "Error reading inventory file: {error}") + } + Error::ProcessPath(error) => write!(f, "Error processing path: {error}"), + Error::ProcessRootPath(error) => write!(f, "Error processing root path: {error}"), + Error::ParseInventory(error) => write!(f, "Error parsing inventory: {error}"), + Error::Filetree(error) => write!(f, "Filetree error: {error}"), + } + } + } +} + +use std::{collections::VecDeque, path::Path}; + +use filetree::Filetree; +use state::State; + +pub fn oneshot() -> Result<(), Box<dyn std::error::Error>> { + let args = args::parse_args().map_err(Error::ParseArgs)?; + let config_dir = + path_processing::process_path(&args.config_dir).map_err(Error::ProcessDirPath)?; + let config_path = config_dir.join("config.txt"); + let config_file = std::fs::read_to_string(&config_path).map_err(Error::ReadConfigFile)?; + let config = config::parse_config(&config_file).map_err(Error::ParseConfig)?; + + logging::setup_logging(log::LevelFilter::Trace); + + log::debug!("Starting with args: {args:#?}"); + log::debug!("Read config: {config:#?}"); + + let state = State::new(&config_dir, config)?; + + log::debug!("Read inventory: {:#?}", state.inventory); + + read(&state)?; + + Ok(()) +} + +/// This meta is used for display tree. +#[derive(Clone, Debug)] +struct Meta { + inventory: Option<Option<String>>, + blacklist: Option<bool>, +} + +/// Scan workspace and display its tree. +fn read(state: &State) -> Result<(), Error> { + let mut filetree = Filetree::new( + "/", + Meta { + blacklist: None, + inventory: None, + }, + ); + + let mut processing_buffer = Vec::new(); + + // Add all whitelist to buffer + for filter in state.filters.iter().filter(|filter| filter.is_whitelist()) { + processing_buffer.push(filter.path.clone()); + } + + // This makes it so every prefix goes first. + processing_buffer.sort(); + + // Has some specific ordering so the tree is built in the correct order. + let mut processing_buffer = VecDeque::from(processing_buffer); + + while let Some(item) = processing_buffer.pop_front() { + tracing::debug!("Adding {:?} to tree", item); + + let allowed_to_scan = allowed_to_scan(state, &item)?; + let inventory_message = (|| -> Result<Option<Option<String>>, Error> { + let Some(node) = state.inventory_tree.get_node_by_path(&item) else { + return Ok(None); + }; + if !node.is_leaf() { + return Ok(None); + } + let Some(check) = node.meta().clone() else { + return Err(Error::NoInventoryCheckInfo); + }; + Ok(Some(match check { + inventory::Check::None => None, + inventory::Check::Shell(cmd) => Some( + String::from_utf8( + std::process::Command::new("sh") + .arg("-c") + .arg(cmd) + .output() + .map_err(Error::Command)? + .stdout, + ) + .map_err(Error::StringUtf8)?, + ), + })) + })()?; + + if let Some(meta) = filetree.get_meta_mut_by_path(&item) { + meta.blacklist = Some(!allowed_to_scan); + } else { + // The earliest prefix that already added + let mut base = item.clone(); + while !filetree.check_path(&base) { + base.pop(); + } + + // What is not yet added + let mut suffix = item + .strip_prefix(&base) + .map_err(Error::BasePrefix)? + .to_owned(); + suffix.pop(); + + for component in suffix.components() { + base.push(component); + filetree + .insert( + base.clone(), + Meta { + blacklist: None, + inventory: None, + }, + ) + .map_err(Error::Filetree)?; + } + filetree + .insert( + item.clone(), + Meta { + blacklist: Some(!allowed_to_scan), + inventory: inventory_message, + }, + ) + .map_err(Error::Filetree)?; + } + + if allowed_to_scan && should_scan(state, &item) { + tracing::debug!("Scanning {:?}", item.file_name()); + let mut iter = std::fs::read_dir(&item).map_err(Error::ReadFs)?; + + let mut dir_buffer = Vec::new(); + + while let Some(result) = iter.next().transpose().map_err(Error::ReadFs)? { + let path = result.path(); + dir_buffer.push(path) + } + + dir_buffer.sort(); + + while let Some(path) = dir_buffer.pop() { + processing_buffer.push_front(path); + } + } + } + + print(&filetree)?; + + Ok(()) +} + +/// Does not check if allowed. +/// Please check if allowed before doing this. +fn should_scan(state: &State, path: &Path) -> bool { + // is allowed + // and is a prefix of a blacklist that is not overridden or is a prefix of an inventory item + let inventory = if let Some(node) = state.inventory_tree.get_node_by_path(path) { + !node.is_leaf() + } else { + false + }; + + state.blacklist_tree.check_path(path) || inventory +} + +/// If a directory is allowed to be scanned. +/// Whitelist has more priority than blacklist on the same level of specificity. +fn allowed_to_scan(state: &State, path: &Path) -> Result<bool, Error> { + // If there is no not-overridden blacklist + + // If no such blacklist filter found that blacklists the path such that + // for this blacklist filter no such whitelist found that whitelists the + // path back aka for which the blacklist is the prefix + Ok(!state + .filters + .iter() + .filter(|filter| filter.is_blacklist()) + .any(|bl_filter| { + path.starts_with(&bl_filter.path) + && !state + .filters + .iter() + .filter(|filter| filter.is_whitelist()) + .any(|wl_filter| { + path.starts_with(&wl_filter.path) + && wl_filter.path.starts_with(&bl_filter.path) + }) + })) +} + +fn print(tree: &Filetree<Meta>) -> Result<(), Error> { + let mut print_buffer = VecDeque::new(); + println!( + "{}", + tree.root().path().to_str().unwrap_or("UNKNOWN").to_owned() + ); + print_buffer.extend( + tree.root() + .children() + .iter() + .copied() + .map(|child_id| (1, child_id)), + ); + while let Some((offset, node_id)) = print_buffer.pop_front() { + let node = tree.get_node(node_id).expect("Shold have the node"); + let name = node + .path() + .file_name() + .map(|name| name.to_str()) + .flatten() + .unwrap_or("UNKNOWN") + .to_owned(); + let offset_text = " ".repeat(offset); + if let Some(msg) = node.meta().inventory.as_ref() { + let msg = msg.as_deref().unwrap_or("\x1b[31mno info\x1b[0m"); + println!( + "{}\x1b[2m|\x1b[0m \x1b[32m{}\x1b[0m \x1b[3m{}\x1b[0m", + offset_text, name, msg, + ); + } else if let Some(true) = node.meta().blacklist { + println!("{}\x1b[2m|\x1b[0m \x1b[2;9m{}\x1b[0m", offset_text, name); + } else if node.is_leaf() { + println!("{}\x1b[2m|\x1b[0m \x1b[31m{}\x1b[0m", offset_text, name); + } else { + println!("{}\x1b[2m|\x1b[0m {}", offset_text, name); + } + for child in node + .children() + .iter() + .copied() + .map(|child_id| (offset + 1, child_id)) + .collect::<Vec<_>>() + .into_iter() + .rev() + { + print_buffer.push_front(child); + } + } + Ok(()) +} + +#[derive(Debug)] +pub enum Error { + ParseArgs(args::Error), + ProcessDirPath(path_processing::Error), + ReadConfigFile(std::io::Error), + ParseConfig(config::Error), + Filetree(filetree::Error), + BasePrefix(std::path::StripPrefixError), + NoInventoryCheckInfo, + Command(std::io::Error), + StringUtf8(std::string::FromUtf8Error), + ReadFs(std::io::Error), +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::ParseArgs(error) => Some(error), + Error::ProcessDirPath(error) => Some(error), + Error::ReadConfigFile(error) => Some(error), + Error::ParseConfig(error) => Some(error), + Error::Filetree(error) => Some(error), + Error::BasePrefix(error) => Some(error), + Error::NoInventoryCheckInfo => None, + Error::Command(error) => Some(error), + Error::StringUtf8(error) => Some(error), + Error::ReadFs(error) => Some(error), + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ParseArgs(error) => write!(f, "Error parsing args: {error}"), + Error::ProcessDirPath(error) => write!(f, "Error processing directory path: {error}"), + Error::ReadConfigFile(error) => write!(f, "Error reading config file: {error}"), + Error::ParseConfig(error) => write!(f, "Error parsing config: {error}"), + Error::Filetree(error) => write!(f, "Filetree error: {error}"), + Error::BasePrefix(error) => write!(f, "Error stripping prefix: {error}"), + Error::NoInventoryCheckInfo => write!(f, "No inventory check info in filetree"), + Error::Command(error) => write!(f, "Error executing command: {error}"), + Error::StringUtf8(error) => write!(f, "Error building utf8 string: {error}"), + Error::ReadFs(error) => write!(f, "Error reading from filesystem: {error}"), + } + } +} diff --git a/src/lib.rs b/src/lib.rs @@ -18,6 +18,7 @@ pub mod args; pub mod config; pub mod filetree; pub mod inventory; +pub mod jannie_new; pub mod logging; pub mod path_processing; pub mod state; diff --git a/src/main.rs b/src/main.rs @@ -1,5 +1,5 @@ fn main() { - if let Err(error) = jannie::result_main() { + if let Err(error) = jannie::jannie_new::oneshot() { eprintln!("{error}"); } }