jannie

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 20361957cbe998c895109f54b380a2aa6b20395d
parent 7413858992f79cdaacd6af77cb588b35d4d1d30e
Author: Andy Khramtsov <>
Date:   Fri, 20 Feb 2026 00:04:48 +0300

feat: support relative paths and tilde

Diffstat:
MCargo.lock | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 1+
Msrc/args.rs | 2+-
Msrc/lib.rs | 45++++++++++++++++++---------------------------
Asrc/path_processing.rs | 33+++++++++++++++++++++++++++++++++
Msrc/state.rs | 37+++++++++++++++++++++++++++++--------
6 files changed, 140 insertions(+), 36 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -288,6 +288,27 @@ dependencies = [ ] [[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] name = "dlv-list" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -473,6 +494,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "shellexpand", "test-log", "thiserror", "tokio", @@ -516,6 +538,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -570,6 +602,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -665,6 +703,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + +[[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -834,6 +883,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] +name = "shellexpand" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +dependencies = [ + "dirs", +] + +[[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -12,6 +12,7 @@ indexmap = "2.13.0" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" serde_yaml = "0.9.34" +shellexpand = "3.1.1" test-log = { version = "0.2.19", features = ["trace"] } thiserror = "2.0.17" tokio = { version = "1.49.0", features = ["rt-multi-thread", "fs"] } diff --git a/src/args.rs b/src/args.rs @@ -5,6 +5,6 @@ use std::path::PathBuf; #[command(about = "Enforce clean workspace", long_about = None)] pub struct Args { /// Configuration and inventory - #[arg(short, long, default_value = ".")] + #[arg(short, long, default_value = "~/.config/jannie")] pub dir: PathBuf, } diff --git a/src/lib.rs b/src/lib.rs @@ -19,12 +19,15 @@ pub mod config; pub mod filetree; pub mod inventory; pub mod logging; +pub mod path_processing; pub mod state; /// `main`, but returns [`Result`]. pub fn result_main() -> Result<(), Error> { let args = Args::parse(); - let config_path = args.dir.join("config.yaml"); + let dir = path_processing::process_path(&args.dir)?; + + let config_path = dir.join("config.yaml"); let config = Config::new(&config_path)?; logging::setup_logging(&config.logging)?; @@ -32,7 +35,7 @@ pub fn result_main() -> Result<(), Error> { tracing::debug!("Starting with args: {args:#?}"); tracing::debug!("Read config: {config:#?}"); - let state = State::new(&args.dir, config)?; + let state = State::new(&dir, config)?; tracing::debug!("Read inventory: {:#?}", state.inventory); @@ -51,7 +54,7 @@ struct Meta { /// Scan workspace and display its tree. async fn read(state: &State) -> Result<(), Error> { let root = Node::new( - state.config.root.clone(), + "/".into(), Meta { blacklist: None, inventory: None, @@ -62,18 +65,9 @@ async fn read(state: &State) -> Result<(), Error> { let mut processing_buffer = Vec::new(); // Add all whitelist to buffer - processing_buffer.extend( - state - .config - .filters - .iter() - .filter(|filter| filter.is_whitelist()) - .map(|filter| { - let mut path = state.config.root.clone(); - path.push(&filter.path); - path - }), - ); + 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(); @@ -112,10 +106,13 @@ async fn read(state: &State) -> Result<(), Error> { if let Some(node) = filetree.node_by_path(&item) { node.try_borrow_mut()?.meta_mut().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)? @@ -184,22 +181,16 @@ fn should_scan(state: &State, path: &Path) -> Result<bool, Error> { fn allowed_to_scan(state: &State, path: &Path) -> Result<bool, Error> { // If there is no not-overridden blacklist - let path = path - .strip_prefix(&state.config.root) - .map_err(Error::RootPrefix)?; - // 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 - .config .filters .iter() .filter(|filter| filter.is_blacklist()) .any(|bl_filter| { path.starts_with(&bl_filter.path) && !state - .config .filters .iter() .filter(|filter| filter.is_whitelist()) @@ -241,15 +232,15 @@ fn print(tree: &Filetree<Meta>) -> Result<(), Error> { if let Some(msg) = node.try_borrow()?.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", + "{}\x1b[2m|\x1b[0m \x1b[32m{}\x1b[0m \x1b[3m{}\x1b[0m", offset_text, name, msg, ); } else if let Some(true) = node.try_borrow()?.meta().blacklist { - println!("{}\x1b[2m|\x1b[0m \x1b[2;9m/{}\x1b[0m", offset_text, name); + println!("{}\x1b[2m|\x1b[0m \x1b[2;9m{}\x1b[0m", offset_text, name); } else if node.try_borrow()?.is_leaf() { - println!("{}\x1b[2m|\x1b[0m \x1b[31m/{}\x1b[0m", offset_text, name); + println!("{}\x1b[2m|\x1b[0m \x1b[31m{}\x1b[0m", offset_text, name); } else { - println!("{}\x1b[2m|\x1b[0m /{}", offset_text, name); + println!("{}\x1b[2m|\x1b[0m {}", offset_text, name); } for child in node .try_borrow()? @@ -271,6 +262,8 @@ pub enum Error { Config(#[from] config::Error), #[error(transparent)] State(#[from] state::Error), + #[error(transparent)] + ProcessPath(#[from] path_processing::Error), #[error("Error setting up logging: {0}")] Logging(#[from] logging::Error), #[error("Error building filetree: {0}")] @@ -279,8 +272,6 @@ pub enum Error { Borrow(#[from] BorrowError), #[error("Borrow mut error: {0}")] BorrowMut(#[from] BorrowMutError), - #[error("Error stripping root from path: {0}")] - RootPrefix(StripPrefixError), #[error("Error stripping base from path: {0}")] BasePrefix(StripPrefixError), #[error("No inventory check info on tree node")] diff --git a/src/path_processing.rs b/src/path_processing.rs @@ -0,0 +1,33 @@ +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()))?, + )?; + 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(thiserror::Error, Debug)] +pub enum Error { + #[error("Error parsing path: {0:?}")] + Parse(PathBuf), + #[error("Error expanding path: {0}")] + Expand(#[from] shellexpand::LookupError<VarError>), + #[error("Error canonicalizing path: {0}")] + Canonicalize(std::io::Error), +} diff --git a/src/state.rs b/src/state.rs @@ -3,9 +3,10 @@ use std::path::{Path, PathBuf}; use tokio::runtime::Runtime; use crate::{ - config::{Config, InventoryFormat}, + config::{Config, Filter, InventoryFormat}, filetree::{self, Filetree, node::Node}, inventory::{self, Inventory}, + path_processing, }; pub struct State { @@ -13,6 +14,10 @@ pub struct State { pub config: Config, pub runtime: Runtime, pub inventory: Inventory, + /// Paths are absolute and processed + pub filters: Vec<Filter>, + /// Absolute processed + pub root: PathBuf, pub blacklist_tree: Filetree<()>, pub whitelist_tree: Filetree<()>, pub inventory_tree: Filetree<Option<inventory::Check>>, @@ -42,23 +47,34 @@ impl State { .map_err(Error::ReadInventoryJson)?, }; - let mut blacklist_tree = Filetree::new(Node::new("/".into(), ())); - let mut whitelist_tree = Filetree::new(Node::new("/".into(), ())); + let root = path_processing::process_path(&config.root)?; + + let mut filters = Vec::new(); for filter in &config.filters { let mut path = config.root.clone(); path.push(&filter.path); + filters.push(Filter { + path: path_processing::process_path(&path)?, + filter_type: filter.filter_type.clone(), + }); + } + + let mut blacklist_tree = Filetree::new(Node::new("/".into(), ())); + let mut whitelist_tree = Filetree::new(Node::new("/".into(), ())); + + for filter in &filters { + let path = &filter.path; match filter.filter_type { crate::config::FilterType::Whitelist => { tracing::debug!("Adding {:?} to whitelist", path); add_to_tree(&mut whitelist_tree, &path)? } crate::config::FilterType::Blacklist => { - if config - .filters + if filters .iter() .filter(|filter| filter.is_whitelist()) - .find(|filter| filter.path == path) + .find(|filter| &filter.path == path) .is_some() { tracing::debug!("Skipping {:?} blacklist", path); @@ -72,7 +88,7 @@ impl State { let mut inventory_tree = Filetree::new(Node::new("/".into(), None)); for item in &inventory.items { - let path = &item.path; + let path = path_processing::process_path(&item.path)?; let mut components = path.components(); let mut path_buf = PathBuf::new(); loop { @@ -86,7 +102,7 @@ impl State { } None => { inventory_tree - .node_by_path(path) + .node_by_path(&path) .expect("Should have added inventory item") .borrow_mut() .meta_mut() @@ -96,11 +112,14 @@ impl State { } } } + Ok(Self { config_dir: config_dir.into(), config, runtime, inventory, + filters, + root, blacklist_tree, whitelist_tree, inventory_tree, @@ -135,6 +154,8 @@ pub enum Error { Runtime(tokio::io::Error), #[error("Error reading file: {0}")] ReadFile(std::io::Error), + #[error("Error processing path: {0}")] + ProcessPath(#[from] path_processing::Error), #[error("Error parsing yaml inventory: {0}")] ReadInventoryYaml(serde_yaml::Error), #[error("Error parsing json inventory: {0}")]