2021-09-29 17:03:46 +02:00
|
|
|
use std::borrow::Cow;
|
2021-09-27 13:54:33 +02:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2021-09-23 11:59:35 +02:00
|
|
|
use std::path::PathBuf;
|
2021-09-28 11:21:13 +02:00
|
|
|
use std::sync::Arc;
|
2021-09-23 11:59:35 +02:00
|
|
|
|
2021-09-29 17:03:46 +02:00
|
|
|
use dunce::canonicalize;
|
2021-09-27 13:54:33 +02:00
|
|
|
use globset::GlobMatcher;
|
|
|
|
use regex::Regex;
|
2021-09-28 11:21:13 +02:00
|
|
|
use tracing::{debug, trace, warn};
|
2021-09-29 15:34:27 +02:00
|
|
|
use unicase::UniCase;
|
2021-09-27 13:54:33 +02:00
|
|
|
|
2021-09-23 11:59:35 +02:00
|
|
|
use crate::error::RuntimeError;
|
2021-09-27 13:54:33 +02:00
|
|
|
use crate::event::{Event, Tag};
|
2021-09-23 11:59:35 +02:00
|
|
|
use crate::filter::Filterer;
|
|
|
|
|
|
|
|
mod parse;
|
2021-09-28 11:21:51 +02:00
|
|
|
pub mod swaplock;
|
2021-10-09 07:41:45 +02:00
|
|
|
pub mod error;
|
2021-09-23 11:59:35 +02:00
|
|
|
|
2021-09-29 17:03:46 +02:00
|
|
|
#[derive(Debug)]
|
2021-09-23 11:59:35 +02:00
|
|
|
pub struct TaggedFilterer {
|
2021-09-27 13:54:33 +02:00
|
|
|
/// The directory the project is in, its "root".
|
|
|
|
///
|
|
|
|
/// This is used to resolve absolute paths without an `in_path` context.
|
2021-09-28 11:21:13 +02:00
|
|
|
root: PathBuf,
|
2021-09-27 13:54:33 +02:00
|
|
|
|
|
|
|
/// Where the program is running from.
|
|
|
|
///
|
|
|
|
/// This is used to resolve relative paths without an `in_path` context.
|
2021-09-28 11:21:13 +02:00
|
|
|
workdir: PathBuf,
|
2021-09-27 13:54:33 +02:00
|
|
|
|
|
|
|
/// All filters that are applied, in order, by matcher.
|
2021-09-28 11:21:13 +02:00
|
|
|
filters: swaplock::SwapLock<HashMap<Matcher, Vec<Filter>>>,
|
2021-09-23 11:59:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Filterer for TaggedFilterer {
|
2021-09-28 11:21:13 +02:00
|
|
|
fn check_event(&self, event: &Event) -> Result<bool, RuntimeError> {
|
2021-10-09 07:41:45 +02:00
|
|
|
self.check(event).map_err(|e| e.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TaggedFilterer {
|
|
|
|
fn check(&self, event: &Event) -> Result<bool, error::TaggedFiltererError> {
|
2021-09-28 11:21:13 +02:00
|
|
|
// TODO: trace logging
|
|
|
|
if self.filters.borrow().is_empty() {
|
|
|
|
trace!("no filters, skipping entire check (pass)");
|
2021-09-27 13:54:33 +02:00
|
|
|
return Ok(true);
|
|
|
|
}
|
|
|
|
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(tags=%event.tags.len(), "checking all tags on the event");
|
2021-09-27 13:54:33 +02:00
|
|
|
for tag in &event.tags {
|
2021-09-28 11:21:13 +02:00
|
|
|
let filters = self.filters.borrow().get(&tag.into()).cloned();
|
|
|
|
if let Some(tag_filters) = filters {
|
|
|
|
trace!(?tag, "checking tag");
|
|
|
|
|
2021-09-27 13:54:33 +02:00
|
|
|
if tag_filters.is_empty() {
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(?tag, "no filters for this tag, skipping (pass)");
|
2021-09-27 13:54:33 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(?tag, filters=%tag_filters.len(), "found some filters for this tag");
|
|
|
|
|
2021-09-27 13:54:33 +02:00
|
|
|
let mut tag_match = true;
|
2021-09-28 11:21:13 +02:00
|
|
|
for filter in &tag_filters {
|
|
|
|
trace!(?filter, ?tag, "checking filter againt tag");
|
2021-09-27 13:54:33 +02:00
|
|
|
if let Some(app) = self.match_tag(filter, tag)? {
|
|
|
|
if filter.negate {
|
|
|
|
if app {
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(prev=%tag_match, now=%true, "negate filter passes, resetting tag to pass");
|
2021-09-27 13:54:33 +02:00
|
|
|
tag_match = true;
|
2021-09-28 11:21:13 +02:00
|
|
|
} else {
|
|
|
|
trace!(prev=%tag_match, now=%tag_match, "negate filter fails, ignoring");
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
|
|
|
} else {
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(prev=%tag_match, this=%app, now=%(tag_match&app), "filter applies to this tag");
|
2021-09-27 13:54:33 +02:00
|
|
|
tag_match &= app;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !tag_match {
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(?tag, "tag fails check, failing entire event");
|
2021-09-27 13:54:33 +02:00
|
|
|
return Ok(false);
|
|
|
|
}
|
2021-09-28 11:21:13 +02:00
|
|
|
|
|
|
|
trace!(?tag, "tag passes check, continuing");
|
|
|
|
} else {
|
|
|
|
trace!(?tag, "no filters for this tag, skipping (pass)");
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(?event, "passing event");
|
2021-09-27 13:54:33 +02:00
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
pub struct Filter {
|
|
|
|
/// Path the filter applies from.
|
|
|
|
pub in_path: Option<PathBuf>,
|
|
|
|
|
|
|
|
/// Which tag the filter applies to.
|
|
|
|
pub on: Matcher,
|
|
|
|
|
|
|
|
/// The operation to perform on the tag's value.
|
|
|
|
pub op: Op,
|
|
|
|
|
|
|
|
/// The pattern to match against the tag's value.
|
|
|
|
pub pat: Pattern,
|
|
|
|
|
|
|
|
/// If true, a positive match with this filter will override negative matches from previous
|
|
|
|
/// filters on the same tag, and negative matches will be ignored.
|
|
|
|
pub negate: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TaggedFilterer {
|
2021-09-29 17:03:46 +02:00
|
|
|
pub fn new(
|
|
|
|
root: impl Into<PathBuf>,
|
|
|
|
workdir: impl Into<PathBuf>,
|
2021-10-09 07:41:45 +02:00
|
|
|
) -> Result<Arc<Self>, error::TaggedFiltererError> {
|
2021-09-29 17:03:46 +02:00
|
|
|
// TODO: make it criticalerror
|
|
|
|
Ok(Arc::new(Self {
|
|
|
|
root: canonicalize(root.into())?,
|
|
|
|
workdir: canonicalize(workdir.into())?,
|
2021-09-28 11:21:51 +02:00
|
|
|
filters: swaplock::SwapLock::new(HashMap::new()),
|
2021-09-29 17:03:46 +02:00
|
|
|
}))
|
2021-09-28 11:21:51 +02:00
|
|
|
}
|
|
|
|
|
2021-09-29 17:03:46 +02:00
|
|
|
// filter ctx event path filter outcome
|
|
|
|
// /foo/bar /foo/bar/baz.txt baz.txt pass
|
|
|
|
// /foo/bar /foo/bar/baz.txt /baz.txt pass
|
|
|
|
// /foo/bar /foo/bar/baz.txt /baz.* pass
|
|
|
|
// /foo/bar /foo/bar/baz.txt /blah fail
|
|
|
|
// /foo/quz /foo/bar/baz.txt /baz.* skip
|
|
|
|
// TODO: lots of tests
|
|
|
|
|
|
|
|
// Ok(Some(bool)) => the match was applied, bool is the result
|
|
|
|
// Ok(None) => for some precondition, the match was not done (mismatched tag, out of context, …)
|
2021-10-09 07:41:45 +02:00
|
|
|
fn match_tag(&self, filter: &Filter, tag: &Tag) -> Result<Option<bool>, error::TaggedFiltererError> {
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(?tag, matcher=?filter.on, "matching filter to tag");
|
2021-09-27 13:54:33 +02:00
|
|
|
match (tag, filter.on) {
|
|
|
|
(tag, Matcher::Tag) => filter.matches(tag.discriminant_name()),
|
2021-09-29 17:03:46 +02:00
|
|
|
(Tag::Path(path), Matcher::Path) => {
|
|
|
|
let resolved = if let Some(ctx) = &filter.in_path {
|
|
|
|
if let Ok(suffix) = path.strip_prefix(ctx) {
|
|
|
|
suffix.strip_prefix("/").unwrap_or(suffix)
|
|
|
|
} else {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
} else if let Ok(suffix) = path.strip_prefix(&self.workdir) {
|
|
|
|
suffix.strip_prefix("/").unwrap_or(suffix)
|
|
|
|
} else if let Ok(suffix) = path.strip_prefix(&self.root) {
|
|
|
|
suffix.strip_prefix("/").unwrap_or(suffix)
|
|
|
|
} else {
|
|
|
|
path.strip_prefix("/").unwrap_or(path)
|
|
|
|
};
|
|
|
|
|
|
|
|
trace!(?resolved, "resolved path to match filter against");
|
|
|
|
filter.matches(resolved.to_string_lossy())
|
|
|
|
}
|
2021-09-28 11:21:13 +02:00
|
|
|
(Tag::FileEventKind(kind), Matcher::FileEventKind) => {
|
|
|
|
filter.matches(format!("{:?}", kind))
|
|
|
|
}
|
2021-09-27 13:54:33 +02:00
|
|
|
(Tag::Source(src), Matcher::Source) => filter.matches(src.to_string()),
|
|
|
|
(Tag::Process(pid), Matcher::Process) => filter.matches(pid.to_string()),
|
|
|
|
(Tag::Signal(_sig), Matcher::Signal) => todo!("tagged filterer: signal matcher"),
|
2021-09-28 11:21:13 +02:00
|
|
|
(Tag::ProcessCompletion(_oes), Matcher::ProcessCompletion) => {
|
|
|
|
todo!("tagged filterer: completion matcher")
|
|
|
|
}
|
|
|
|
(tag, matcher) => {
|
|
|
|
trace!(?tag, ?matcher, "no match for tag, skipping");
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.map(Some)
|
|
|
|
}
|
|
|
|
|
2021-10-09 07:41:45 +02:00
|
|
|
pub async fn add_filter(&self, mut filter: Filter) -> Result<(), error::TaggedFiltererError> {
|
2021-09-28 11:21:51 +02:00
|
|
|
debug!(?filter, "adding filter to filterer");
|
2021-09-29 17:03:46 +02:00
|
|
|
|
|
|
|
if let Some(ctx) = &mut filter.in_path {
|
|
|
|
*ctx = canonicalize(&ctx)?;
|
|
|
|
trace!(canon=?ctx, "canonicalised in_path");
|
|
|
|
}
|
|
|
|
|
2021-09-28 11:21:51 +02:00
|
|
|
self.filters
|
|
|
|
.change(|filters| {
|
|
|
|
filters.entry(filter.on).or_default().push(filter);
|
|
|
|
})
|
|
|
|
.await
|
2021-10-09 07:41:45 +02:00
|
|
|
.map_err(|err| error::TaggedFiltererError::FilterChange { action: "add", err })?;
|
2021-09-28 11:21:51 +02:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-10-09 07:41:45 +02:00
|
|
|
pub async fn remove_filter(&self, filter: &Filter) -> Result<(), error::TaggedFiltererError> {
|
2021-09-29 17:03:46 +02:00
|
|
|
let filter = if let Some(ctx) = &filter.in_path {
|
|
|
|
let f = filter.clone();
|
|
|
|
Cow::Owned(Filter {
|
|
|
|
in_path: Some(canonicalize(ctx)?),
|
|
|
|
..f
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
Cow::Borrowed(filter)
|
|
|
|
};
|
|
|
|
|
2021-09-28 11:21:51 +02:00
|
|
|
debug!(?filter, "removing filter from filterer");
|
|
|
|
self.filters
|
|
|
|
.change(|filters| {
|
|
|
|
filters
|
|
|
|
.entry(filter.on)
|
|
|
|
.or_default()
|
2021-09-29 17:03:46 +02:00
|
|
|
.retain(|f| f != filter.as_ref());
|
2021-09-28 11:21:51 +02:00
|
|
|
})
|
|
|
|
.await
|
2021-10-09 07:41:45 +02:00
|
|
|
.map_err(|err| error::TaggedFiltererError::FilterChange {
|
2021-09-28 11:21:51 +02:00
|
|
|
action: "remove",
|
|
|
|
err,
|
|
|
|
})?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-10-09 07:41:45 +02:00
|
|
|
pub async fn clear_filters(&self) -> Result<(), error::TaggedFiltererError> {
|
2021-09-28 11:21:51 +02:00
|
|
|
debug!("removing all filters from filterer");
|
|
|
|
self.filters
|
|
|
|
.replace(Default::default())
|
|
|
|
.await
|
2021-10-09 07:41:45 +02:00
|
|
|
.map_err(|err| error::TaggedFiltererError::FilterChange {
|
2021-09-28 11:21:51 +02:00
|
|
|
action: "clear all",
|
|
|
|
err,
|
|
|
|
})?;
|
|
|
|
Ok(())
|
2021-09-23 11:59:35 +02:00
|
|
|
}
|
|
|
|
}
|
2021-09-27 13:54:33 +02:00
|
|
|
|
|
|
|
impl Filter {
|
2021-09-29 17:03:46 +02:00
|
|
|
// TODO non-unicode matching
|
2021-10-09 07:41:45 +02:00
|
|
|
pub fn matches(&self, subject: impl AsRef<str>) -> Result<bool, error::TaggedFiltererError> {
|
2021-09-27 13:54:33 +02:00
|
|
|
let subject = subject.as_ref();
|
|
|
|
|
2021-09-28 11:21:13 +02:00
|
|
|
trace!(op=?self.op, pat=?self.pat, ?subject, "performing filter match");
|
|
|
|
Ok(match (self.op, &self.pat) {
|
2021-09-29 15:34:27 +02:00
|
|
|
(Op::Equal, Pattern::Exact(pat)) => UniCase::new(subject) == UniCase::new(pat),
|
|
|
|
(Op::NotEqual, Pattern::Exact(pat)) => UniCase::new(subject) != UniCase::new(pat),
|
2021-09-28 11:21:13 +02:00
|
|
|
(Op::Regex, Pattern::Regex(pat)) => pat.is_match(subject),
|
|
|
|
(Op::NotRegex, Pattern::Regex(pat)) => !pat.is_match(subject),
|
|
|
|
(Op::Glob, Pattern::Glob(pat)) => pat.is_match(subject),
|
|
|
|
(Op::NotGlob, Pattern::Glob(pat)) => !pat.is_match(subject),
|
|
|
|
(Op::InSet, Pattern::Set(set)) => set.contains(subject),
|
|
|
|
(Op::InSet, Pattern::Exact(pat)) => subject == pat,
|
|
|
|
(Op::NotInSet, Pattern::Set(set)) => !set.contains(subject),
|
|
|
|
(Op::NotInSet, Pattern::Exact(pat)) => subject != pat,
|
2021-09-27 13:54:33 +02:00
|
|
|
(op, pat) => {
|
2021-09-28 11:21:13 +02:00
|
|
|
warn!(
|
|
|
|
"trying to match pattern {:?} with op {:?}, that cannot work",
|
|
|
|
pat, op
|
|
|
|
);
|
|
|
|
false
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
2021-09-28 11:21:13 +02:00
|
|
|
})
|
2021-09-27 13:54:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
|
|
|
#[non_exhaustive]
|
|
|
|
pub enum Matcher {
|
|
|
|
Tag,
|
|
|
|
Path,
|
|
|
|
FileEventKind,
|
|
|
|
Source,
|
|
|
|
Process,
|
|
|
|
Signal,
|
|
|
|
ProcessCompletion,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&Tag> for Matcher {
|
|
|
|
fn from(tag: &Tag) -> Self {
|
|
|
|
match tag {
|
|
|
|
Tag::Path(_) => Matcher::Path,
|
|
|
|
Tag::FileEventKind(_) => Matcher::FileEventKind,
|
|
|
|
Tag::Source(_) => Matcher::Source,
|
|
|
|
Tag::Process(_) => Matcher::Process,
|
|
|
|
Tag::Signal(_) => Matcher::Signal,
|
|
|
|
Tag::ProcessCompletion(_) => Matcher::ProcessCompletion,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
#[non_exhaustive]
|
|
|
|
pub enum Op {
|
|
|
|
Auto, // =
|
|
|
|
Equal, // ==
|
|
|
|
NotEqual, // !=
|
|
|
|
Regex, // ~=
|
|
|
|
NotRegex, // ~!
|
|
|
|
Glob, // *=
|
|
|
|
NotGlob, // *!
|
|
|
|
InSet, // :=
|
|
|
|
NotInSet, // :!
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
#[non_exhaustive]
|
|
|
|
pub enum Pattern {
|
|
|
|
Exact(String),
|
|
|
|
Regex(Regex),
|
|
|
|
Glob(GlobMatcher),
|
|
|
|
Set(HashSet<String>),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PartialEq<Self> for Pattern {
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
match (self, other) {
|
|
|
|
(Self::Exact(l), Self::Exact(r)) => l == r,
|
|
|
|
(Self::Regex(l), Self::Regex(r)) => l.as_str() == r.as_str(),
|
|
|
|
(Self::Glob(l), Self::Glob(r)) => l.glob() == r.glob(),
|
|
|
|
(Self::Set(l), Self::Set(r)) => l == r,
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Eq for Pattern {}
|