commit 20361957cbe998c895109f54b380a2aa6b20395d
parent 7413858992f79cdaacd6af77cb588b35d4d1d30e
Author: Andy Khramtsov <>
Date: Fri, 20 Feb 2026 00:04:48 +0300
feat: support relative paths and tilde
Diffstat:
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}")]