diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/csl.rs | 34 | ||||
| -rw-r--r-- | src/filters.rs | 396 | ||||
| -rw-r--r-- | src/main.rs | 43 | 
3 files changed, 397 insertions, 76 deletions
| @@ -1,14 +1,14 @@  use std::collections::HashSet; +use std::fmt;  use std::hash::Hash;  use std::str::FromStr; -use std::fmt; -use super::{SearchField, FightOutcome}; +use super::{FightOutcome, SearchField};  use chrono::Weekday;  use evtclib::statistics::gamedata::Boss;  pub trait Variants: Copy { -    type Output: Iterator<Item=Self>; +    type Output: Iterator<Item = Self>;      fn variants() -> Self::Output;  } @@ -79,18 +79,21 @@ impl<E: fmt::Display> fmt::Display for ParseError<E> {  }  impl<T> FromStr for CommaSeparatedList<T> -    where T: FromStr + Variants + Hash + Eq + fmt::Debug +where +    T: FromStr + Variants + Hash + Eq + fmt::Debug,  {      type Err = ParseError<T::Err>;      fn from_str(input: &str) -> Result<Self, Self::Err> {          if input == "*" { -            Ok(CommaSeparatedList { values: T::variants().collect() }) +            Ok(CommaSeparatedList { +                values: T::variants().collect(), +            })          } else if input.starts_with(NEGATOR) {              let no_csl = CommaSeparatedList::from_str(&input[1..])?;              let all_values = T::variants().collect::<HashSet<_>>();              Ok(CommaSeparatedList { -                values: all_values.difference(&no_csl.values).cloned().collect() +                values: all_values.difference(&no_csl.values).cloned().collect(),              })          } else {              let parts = input.split(DELIMITER); @@ -104,9 +107,26 @@ impl<T> FromStr for CommaSeparatedList<T>  }  impl<T> CommaSeparatedList<T> -    where T: Hash + Eq + fmt::Debug +where +    T: Hash + Eq + fmt::Debug,  {      pub fn contains(&self, value: &T) -> bool {          self.values.contains(value)      } + +    pub fn values(&self) -> &HashSet<T> { +        &self.values +    } +} + +// We allow implicit hasher because then it's a zero-cost conversion, as we're just unwrapping the +// values. +#[allow(clippy::implicit_hasher)] +impl<T> From<CommaSeparatedList<T>> for HashSet<T> +where +    T: Hash + Eq + fmt::Debug, +{ +    fn from(csl: CommaSeparatedList<T>) -> Self { +        csl.values +    }  } diff --git a/src/filters.rs b/src/filters.rs index cdd8f36..7da19ec 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,79 +1,361 @@ +#![allow(clippy::new_ret_no_self)] +use std::collections::HashSet; +use std::ops; +  use evtclib::raw::parser::PartialEvtc;  use evtclib::statistics::gamedata::Boss;  use evtclib::{Agent, AgentName}; -use num_traits::FromPrimitive; - -use super::{guilds, LogResult, Opt, SearchField}; - -use chrono::Datelike; - -/// Do filtering based on the character or account name. -pub fn filter_name(evtc: &PartialEvtc, opt: &Opt) -> bool { -    for player in &evtc.agents { -        let fancy = Agent::from_raw(player); -        if let Ok(AgentName::Player { -            ref account_name, -            ref character_name, -            .. -        }) = fancy.as_ref().map(Agent::name) -        { -            if (opt.field.contains(&SearchField::Account) && opt.expression.is_match(account_name)) -                || (opt.field.contains(&SearchField::Character) -                    && opt.expression.is_match(character_name)) +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use regex::Regex; + +use super::{guilds, FightOutcome, LogResult, SearchField, Weekday}; + +use chrono::{Datelike, NaiveDateTime}; + +/// Early filtering result. +/// +/// This implements a [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic), +/// similar to SQL's `NULL`. +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(i8)] +pub enum Inclusion { +    /// The log should be included. +    Include = 1, +    /// The state is yet unknown. +    Unknown = 0, +    /// The log should be excluded. +    Exclude = -1, +} + +impl ops::Not for Inclusion { +    type Output = Self; + +    fn not(self) -> Self::Output { +        Inclusion::from_i8(-(self as i8)).unwrap() +    } +} + +impl ops::BitAnd<Inclusion> for Inclusion { +    type Output = Self; + +    fn bitand(self, rhs: Inclusion) -> Self::Output { +        Inclusion::from_i8((self as i8).min(rhs as i8)).unwrap() +    } +} + +impl ops::BitOr<Inclusion> for Inclusion { +    type Output = Self; + +    fn bitor(self, rhs: Inclusion) -> Self::Output { +        Inclusion::from_i8((self as i8).max(rhs as i8)).unwrap() +    } +} + +impl From<bool> for Inclusion { +    fn from(data: bool) -> Self { +        if data { +            Inclusion::Include +        } else { +            Inclusion::Exclude +        } +    } +} + +/// The main filter trait. +/// +/// Filters are usually handled as a `Box<dyn Filter>`. +pub trait Filter: Send + Sync { +    /// Determine early (before processing all events) whether the log stands a chance to be +    /// included. +    /// +    /// Note that you can return [Inclusion::Unkown] if this filter cannot determine yet a definite +    /// answer. +    fn filter_early(&self, _: &PartialEvtc) -> Inclusion { +        Inclusion::Unknown +    } + +    /// Return whether the log should be included, according to this filter. +    fn filter(&self, log: &LogResult) -> bool; +} + +#[derive(Debug, Clone, Copy)] +pub struct Const(pub bool); + +impl Const { +    pub fn new(output: bool) -> Box<dyn Filter> { +        Box::new(Const(output)) +    } +} + +impl Filter for Const { +    fn filter_early(&self, _: &PartialEvtc) -> Inclusion { +        self.0.into() +    } + +    fn filter(&self, _: &LogResult) -> bool { +        self.0 +    } +} + +struct AndFilter(Box<dyn Filter>, Box<dyn Filter>); + +impl Filter for AndFilter { +    fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { +        let lhs = self.0.filter_early(partial_evtc); +        // Short circuit behaviour +        if lhs == Inclusion::Exclude { +            Inclusion::Exclude +        } else { +            lhs & self.1.filter_early(partial_evtc) +        } +    } + +    fn filter(&self, log: &LogResult) -> bool { +        self.0.filter(log) && self.1.filter(log) +    } +} + +impl ops::BitAnd<Box<dyn Filter>> for Box<dyn Filter> { +    type Output = Box<dyn Filter>; + +    fn bitand(self, rhs: Box<dyn Filter>) -> Self::Output { +        Box::new(AndFilter(self, rhs)) +    } +} + +struct OrFilter(Box<dyn Filter>, Box<dyn Filter>); + +impl Filter for OrFilter { +    fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { +        let lhs = self.0.filter_early(partial_evtc); +        // Short circuit behaviour +        if lhs == Inclusion::Include { +            Inclusion::Include +        } else { +            lhs | self.1.filter_early(partial_evtc) +        } +    } + +    fn filter(&self, log: &LogResult) -> bool { +        self.0.filter(log) || self.1.filter(log) +    } +} + +impl ops::BitOr<Box<dyn Filter>> for Box<dyn Filter> { +    type Output = Box<dyn Filter>; + +    fn bitor(self, rhs: Box<dyn Filter>) -> Self::Output { +        Box::new(OrFilter(self, rhs)) +    } +} + +struct NotFilter(Box<dyn Filter>); + +impl Filter for NotFilter { +    fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { +        !self.0.filter_early(partial_evtc) +    } + +    fn filter(&self, log: &LogResult) -> bool { +        !self.0.filter(log) +    } +} + +impl ops::Not for Box<dyn Filter> { +    type Output = Box<dyn Filter>; + +    fn not(self) -> Self::Output { +        Box::new(NotFilter(self)) +    } +} + +// From here onwards, we have the specific filters + +/// Filter that filters according to the name. +/// +/// The given SearchField determines in which field something should be searched. +#[derive(Debug, Clone)] +pub struct NameFilter(SearchField, Regex); + +impl NameFilter { +    pub fn new(field: SearchField, regex: Regex) -> Box<dyn Filter> { +        Box::new(NameFilter(field, regex)) +    } +} + +impl Filter for NameFilter { +    fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { +        if self.0 == SearchField::Guild { +            return Inclusion::Unknown; +        } + +        for player in &partial_evtc.agents { +            let fancy = Agent::from_raw(player); +            if let Ok(AgentName::Player { +                ref account_name, +                ref character_name, +                .. +            }) = fancy.as_ref().map(Agent::name)              { -                return true; +                let field = match self.0 { +                    SearchField::Account => account_name, +                    SearchField::Character => character_name, +                    _ => unreachable!("We already checked for Guild earlier"), +                }; +                if self.1.is_match(field) { +                    return Inclusion::Include; +                } +            } +        } +        Inclusion::Exclude +    } + +    fn filter(&self, log: &LogResult) -> bool { +        for player in &log.players { +            match self.0 { +                SearchField::Account if self.1.is_match(&player.account_name) => return true, +                SearchField::Character if self.1.is_match(&player.character_name) => return true, +                SearchField::Guild => { +                    let guild_ok = player +                        .guild_id +                        .as_ref() +                        .and_then(|id| guilds::lookup(id)) +                        .map(|guild| self.1.is_match(guild.tag()) || self.1.is_match(guild.name())) +                        .unwrap_or(false); +                    if guild_ok { +                        return true; +                    } +                } +                _ => (),              }          } +        false +    } +} + +#[derive(Debug, Clone)] +pub struct BossFilter(HashSet<Boss>); + +impl BossFilter { +    pub fn new(bosses: HashSet<Boss>) -> Box<dyn Filter> { +        Box::new(BossFilter(bosses)) +    } +} + +impl Filter for BossFilter { +    fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { +        let boss = Boss::from_u16(partial_evtc.header.combat_id); +        boss.map(|b| self.0.contains(&b).into()) +            .unwrap_or(Inclusion::Include) +    } + +    fn filter(&self, log: &LogResult) -> bool { +        let boss = Boss::from_u16(log.boss_id); +        boss.map(|b| self.0.contains(&b)).unwrap_or(false)      } -    // Don't throw away the log yet if we are searching for guilds -    opt.field.contains(&SearchField::Guild)  } -/// Do filtering based on the boss ID. -pub fn filter_boss(evtc: &PartialEvtc, opt: &Opt) -> bool { -    let boss = Boss::from_u16(evtc.header.combat_id); -    boss.map(|b| opt.bosses.contains(&b)).unwrap_or(true) +#[derive(Debug, Clone)] +pub struct OutcomeFilter(HashSet<FightOutcome>); + +impl OutcomeFilter { +    pub fn new(outcomes: HashSet<FightOutcome>) -> Box<dyn Filter> { +        Box::new(OutcomeFilter(outcomes)) +    }  } -/// Do filtering based on the fight outcome. -pub fn filter_outcome(result: &LogResult, opt: &Opt) -> bool { -    opt.outcome.contains(&result.outcome) +impl Filter for OutcomeFilter { +    fn filter(&self, log: &LogResult) -> bool { +        self.0.contains(&log.outcome) +    }  } -/// Do filtering based on the weekday of the fight. -pub fn filter_weekday(result: &LogResult, opt: &Opt) -> bool { -    opt.weekdays.contains(&result.time.weekday()) +#[derive(Debug, Clone)] +pub struct WeekdayFilter(HashSet<Weekday>); + +impl WeekdayFilter { +    pub fn new(weekdays: HashSet<Weekday>) -> Box<dyn Filter> { +        Box::new(WeekdayFilter(weekdays)) +    }  } -/// Do filtering based on encounter time. -pub fn filter_time(result: &LogResult, opt: &Opt) -> bool { -    let after_ok = match opt.after { -        Some(time) => time <= result.time, -        None => true, -    }; -    let before_ok = match opt.before { -        Some(time) => time >= result.time, -        None => true, -    }; +impl Filter for WeekdayFilter { +    fn filter(&self, log: &LogResult) -> bool { +        self.0.contains(&log.time.weekday()) +    } +} -    after_ok && before_ok +#[derive(Debug, Clone)] +pub struct TimeFilter(Option<NaiveDateTime>, Option<NaiveDateTime>); + +impl TimeFilter { +    pub fn new(after: Option<NaiveDateTime>, before: Option<NaiveDateTime>) -> Box<dyn Filter> { +        Box::new(TimeFilter(after, before)) +    }  } -/// Do filtering based on the guilds. -pub fn filter_guilds(result: &LogResult, opt: &Opt) -> bool { -    if !opt.guilds { -        return true; +impl Filter for TimeFilter { +    fn filter(&self, log: &LogResult) -> bool { +        let after_ok = match self.0 { +            Some(time) => time <= log.time, +            None => true, +        }; +        let before_ok = match self.1 { +            Some(time) => time >= log.time, +            None => true, +        }; + +        after_ok && before_ok      } -    if !opt.field.contains(&SearchField::Guild) { -        return true; +} + +#[cfg(test)] +mod tests { +    use super::*; + +    #[test] +    fn test_inclusion_not() { +        use Inclusion::*; + +        assert_eq!(!Exclude, Include); +        assert_eq!(!Include, Exclude); +        assert_eq!(!Unknown, Unknown); +    } + +    #[test] +    fn test_inclusion_and() { +        use Inclusion::*; + +        assert_eq!(Exclude & Exclude, Exclude); +        assert_eq!(Exclude & Unknown, Exclude); +        assert_eq!(Exclude & Include, Exclude); + +        assert_eq!(Unknown & Exclude, Exclude); +        assert_eq!(Unknown & Unknown, Unknown); +        assert_eq!(Unknown & Include, Unknown); + +        assert_eq!(Include & Exclude, Exclude); +        assert_eq!(Include & Unknown, Unknown); +        assert_eq!(Include & Include, Include); +    } + +    #[test] +    fn test_inclusion_or() { +        use Inclusion::*; + +        assert_eq!(Exclude | Exclude, Exclude); +        assert_eq!(Exclude | Unknown, Unknown); +        assert_eq!(Exclude | Include, Include); + +        assert_eq!(Unknown | Exclude, Unknown); +        assert_eq!(Unknown | Unknown, Unknown); +        assert_eq!(Unknown | Include, Include); + +        assert_eq!(Include | Exclude, Include); +        assert_eq!(Include | Unknown, Include); +        assert_eq!(Include | Include, Include);      } -    result.players.iter().any(|player| { -        let guild = player.guild_id.as_ref().and_then(|id| guilds::lookup(id)); -        if let Some(guild) = guild { -            opt.expression.is_match(guild.tag()) || opt.expression.is_match(guild.name()) -        } else { -            false -        } -    })  } diff --git a/src/main.rs b/src/main.rs index 6b67875..41df732 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,15 +6,16 @@ use std::str::FromStr;  use anyhow::{anyhow, Result};  use chrono::{Duration, NaiveDateTime, Weekday}; +use log::debug;  use num_traits::cast::FromPrimitive;  use regex::Regex;  use structopt::StructOpt;  use walkdir::{DirEntry, WalkDir}; -use log::debug;  use evtclib::{AgentKind, AgentName, EventKind, Log};  mod filters; +use filters::{Filter, Inclusion};  mod guilds;  mod logger;  mod output; @@ -32,7 +33,6 @@ macro_rules! unwrap {      };  } -  /// A program that allows you to search through all your evtc logs for specific  /// people.  #[derive(StructOpt, Debug)] @@ -106,7 +106,7 @@ pub struct Opt {  /// A flag indicating which fields should be searched.  #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -enum SearchField { +pub enum SearchField {      /// Only search the account name.      Account,      /// Only search the character name. @@ -135,6 +135,8 @@ pub struct LogResult {      log_file: PathBuf,      /// The time of the recording.      time: NaiveDateTime, +    /// The numeric ID of the boss. +    boss_id: u16,      /// The name of the boss.      boss_name: String,      /// A vector of all participating players. @@ -258,16 +260,36 @@ fn is_log_file(entry: &DirEntry) -> bool {          .unwrap_or(false)  } +fn build_filter(opt: &Opt) -> Box<dyn Filter> { +    let mut filter = filters::Const::new(false); +    for field in opt.field.values() { +        filter = filter | filters::NameFilter::new(*field, opt.expression.clone()); +    } + +    if opt.invert { +        filter = !filter; +    } + +    filter = filter +        & filters::BossFilter::new(opt.bosses.values().clone()) +        & filters::OutcomeFilter::new(opt.outcome.values().clone()) +        & filters::WeekdayFilter::new(opt.weekdays.values().clone()) +        & filters::TimeFilter::new(opt.after, opt.before); + +    filter +} +  /// Run the grep search with the given options.  fn grep(opt: &Opt) -> Result<()> {      let pipeline = &output::build_pipeline(opt); +    let filter: &dyn Filter = &*build_filter(opt);      rayon::scope(|s| {          let walker = WalkDir::new(&opt.path);          for entry in walker {              let entry = entry?;              s.spawn(move |_| {                  if is_log_file(&entry) { -                    let search = search_log(&entry, opt); +                    let search = search_log(&entry, filter);                      match search {                          Ok(None) => (),                          Ok(Some(result)) => pipeline.push_item(&result), @@ -287,7 +309,7 @@ fn grep(opt: &Opt) -> Result<()> {  /// If the log matches, returns `Ok(Some(..))`.  /// If the log doesn't match, returns `Ok(None)`.  /// If there was a fatal error, returns `Err(..)`. -fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> { +fn search_log(entry: &DirEntry, filter: &dyn Filter) -> Result<Option<LogResult>> {      let file_stream = BufReader::new(File::open(entry.path())?);      let is_zip = entry          .file_name() @@ -302,10 +324,9 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> {      let mut stream = wrapper.get_stream();      let partial = evtclib::raw::parser::parse_partial_file(&mut stream)?; -    let early_ok = -        filters::filter_name(&partial, opt) != opt.invert && filters::filter_boss(&partial, opt); +    let early_ok = filter.filter_early(&partial); -    if !early_ok { +    if early_ok == Inclusion::Exclude {          return Ok(None);      } @@ -320,10 +341,7 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> {      let info = extract_info(entry, &log); -    let take_log = filters::filter_outcome(&info, opt) -        && filters::filter_weekday(&info, opt) -        && filters::filter_time(&info, opt) -        && filters::filter_guilds(&info, opt); +    let take_log = filter.filter(&info);      if take_log {          Ok(Some(info)) @@ -372,6 +390,7 @@ fn extract_info(entry: &DirEntry, log: &Log) -> LogResult {      LogResult {          log_file: entry.path().to_path_buf(),          time: NaiveDateTime::from_timestamp(i64::from(get_start_timestamp(log)), 0), +        boss_id: log.boss_id(),          boss_name,          players,          outcome: get_fight_outcome(log), | 
