commit a33457a58df5ef8da428b277742e7306eee9a6ca
parent 2e85004b82cf51993a9771426f3a7a047a50e609
Author: Andy Khramtsov <>
Date: Tue, 14 Jul 2026 01:34:03 +0300
feat: new implementation
Diffstat:
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}");
}
}