bottom/src/app/query.rs

856 lines
34 KiB
Rust

use std::fmt::Debug;
use std::time::Duration;
use std::{borrow::Cow, collections::VecDeque};
use humantime::parse_duration;
use super::data_harvester::processes::ProcessHarvest;
use crate::utils::error::{
BottomError::{self, QueryError},
Result,
};
const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
const OR_LIST: [&str; 2] = ["or", "||"];
const AND_LIST: [&str; 2] = ["and", "&&"];
/// In charge of parsing the given query.
/// We are defining the following language for a query (case-insensitive prefixes):
///
/// - Process names: No prefix required, can use regex, match word, or case.
/// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
/// rather than a prefix.
/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare.
/// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare.
/// - STATE: Use prefix `state`, can use regex, match word, or case.
/// - USER: Use prefix `user`, can use regex, match word, or case.
/// - Read/s: Use prefix `r`. Can compare.
/// - Write/s: Use prefix `w`. Can compare.
/// - Total read: Use prefix `read`. Can compare.
/// - Total write: Use prefix `write`. Can compare.
///
/// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed
/// or quoted elements after splitting to treat as process names.
/// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
pub fn parse_query(
search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> Result<Query> {
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
let lhs = process_or(query)?;
let mut list_of_ors = vec![lhs];
while query.front().is_some() {
list_of_ors.push(process_or(query)?);
}
Ok(Query { query: list_of_ors })
}
fn process_or(query: &mut VecDeque<String>) -> Result<Or> {
let mut lhs = process_and(query)?;
let mut rhs: Option<Box<And>> = None;
while let Some(queue_top) = query.front() {
// debug!("OR QT: {:?}", queue_top);
if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
query.pop_front();
rhs = Some(Box::new(process_and(query)?));
if let Some(queue_next) = query.front() {
if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
// Must merge LHS and RHS
lhs = And {
lhs: Prefix {
or: Some(Box::new(Or { lhs, rhs })),
regex_prefix: None,
compare_prefix: None,
},
rhs: None,
};
rhs = None;
}
} else {
break;
}
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
} else {
break;
}
}
Ok(Or { lhs, rhs })
}
fn process_and(query: &mut VecDeque<String>) -> Result<And> {
let mut lhs = process_prefix(query, false)?;
let mut rhs: Option<Box<Prefix>> = None;
while let Some(queue_top) = query.front() {
// debug!("AND QT: {:?}", queue_top);
if AND_LIST.contains(&queue_top.to_lowercase().as_str()) {
query.pop_front();
rhs = Some(Box::new(process_prefix(query, false)?));
if let Some(next_queue_top) = query.front() {
if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) {
// Must merge LHS and RHS
lhs = Prefix {
or: Some(Box::new(Or {
lhs: And { lhs, rhs },
rhs: None,
})),
regex_prefix: None,
compare_prefix: None,
};
rhs = None;
} else {
break;
}
} else {
break;
}
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
} else {
break;
}
}
Ok(And { lhs, rhs })
}
fn process_prefix(query: &mut VecDeque<String>, inside_quotation: bool) -> Result<Prefix> {
if let Some(queue_top) = query.pop_front() {
if inside_quotation {
if queue_top == "\"" {
// This means we hit something like "". Return an empty prefix, and to deal with
// the close quote checker, add one to the top of the stack. Ugly fix but whatever.
query.push_front("\"".to_string());
return Ok(Prefix {
or: None,
regex_prefix: Some((
PrefixType::Name,
StringQuery::Value(String::default()),
)),
compare_prefix: None,
});
} else {
let mut quoted_string = queue_top;
while let Some(next_str) = query.front() {
if next_str == "\"" {
// Stop!
break;
} else {
quoted_string.push_str(next_str);
query.pop_front();
}
}
return Ok(Prefix {
or: None,
regex_prefix: Some((PrefixType::Name, StringQuery::Value(quoted_string))),
compare_prefix: None,
});
}
} else if queue_top == "(" {
if query.is_empty() {
return Err(QueryError(Cow::Borrowed("Missing closing parentheses")));
}
let mut list_of_ors = VecDeque::new();
while let Some(in_paren_query_top) = query.front() {
if in_paren_query_top != ")" {
list_of_ors.push_back(process_or(query)?);
} else {
break;
}
}
// Ensure not empty
if list_of_ors.is_empty() {
return Err(QueryError("No values within parentheses group".into()));
}
// Now convert this back to a OR...
let initial_or = Or {
lhs: And {
lhs: Prefix {
or: list_of_ors.pop_front().map(Box::new),
compare_prefix: None,
regex_prefix: None,
},
rhs: None,
},
rhs: None,
};
let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {
lhs: And {
lhs: Prefix {
or: Some(Box::new(lhs)),
compare_prefix: None,
regex_prefix: None,
},
rhs: Some(Box::new(Prefix {
or: Some(Box::new(rhs)),
compare_prefix: None,
regex_prefix: None,
})),
},
rhs: None,
});
if let Some(close_paren) = query.pop_front() {
if close_paren == ")" {
return Ok(Prefix {
or: Some(Box::new(returned_or)),
regex_prefix: None,
compare_prefix: None,
});
} else {
return Err(QueryError("Missing closing parentheses".into()));
}
} else {
return Err(QueryError("Missing closing parentheses".into()));
}
} else if queue_top == ")" {
return Err(QueryError("Missing opening parentheses".into()));
} else if queue_top == "\"" {
// Similar to parentheses, trap and check for missing closing quotes. Note, however, that we
// will DIRECTLY call another process_prefix call...
let prefix = process_prefix(query, true)?;
if let Some(close_paren) = query.pop_front() {
if close_paren == "\"" {
return Ok(prefix);
} else {
return Err(QueryError("Missing closing quotation".into()));
}
} else {
return Err(QueryError("Missing closing quotation".into()));
}
} else {
// Get prefix type...
let prefix_type = queue_top.parse::<PrefixType>()?;
let content = if let PrefixType::Name = prefix_type {
Some(queue_top)
} else {
query.pop_front()
};
if let Some(content) = content {
match &prefix_type {
PrefixType::Name => {
return Ok(Prefix {
or: None,
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
compare_prefix: None,
})
}
PrefixType::Pid | PrefixType::State | PrefixType::User => {
// We have to check if someone put an "="...
if content == "=" {
// Check next string if possible
if let Some(queue_next) = query.pop_front() {
// TODO: [Query] Need to consider the following cases:
// - (test)
// - (test
// - test)
// These are split into 2 to 3 different strings due to parentheses being
// delimiters in our query system.
//
// Do we want these to be valid? They should, as a string, right?
return Ok(Prefix {
or: None,
regex_prefix: Some((
prefix_type,
StringQuery::Value(queue_next),
)),
compare_prefix: None,
});
}
} else {
return Ok(Prefix {
or: None,
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
compare_prefix: None,
});
}
}
PrefixType::Time => {
let mut condition: Option<QueryComparison> = None;
let mut duration_string: Option<String> = None;
if content == "=" {
condition = Some(QueryComparison::Equal);
duration_string = query.pop_front();
} else if content == ">" || content == "<" {
if let Some(queue_next) = query.pop_front() {
if queue_next == "=" {
condition = Some(if content == ">" {
QueryComparison::GreaterOrEqual
} else {
QueryComparison::LessOrEqual
});
duration_string = query.pop_front();
} else {
condition = Some(if content == ">" {
QueryComparison::Greater
} else {
QueryComparison::Less
});
duration_string = Some(queue_next);
}
} else {
return Err(QueryError("Missing value".into()));
}
}
if let Some(condition) = condition {
let duration = parse_duration(
&duration_string.ok_or(QueryError("Missing value".into()))?,
)
.map_err(|err| QueryError(err.to_string().into()))?;
return Ok(Prefix {
or: None,
regex_prefix: None,
compare_prefix: Some((
prefix_type,
ComparableQuery::Time(TimeQuery {
condition,
duration,
}),
)),
});
}
}
_ => {
// Assume it's some numerical value.
// Now we gotta parse the content... yay.
let mut condition: Option<QueryComparison> = None;
let mut value: Option<f64> = None;
// TODO: Jeez, what the heck did I write here... add some tests and clean this up in the
// future.
if content == "=" {
condition = Some(QueryComparison::Equal);
if let Some(queue_next) = query.pop_front() {
value = queue_next.parse::<f64>().ok();
} else {
return Err(QueryError("Missing value".into()));
}
} else if content == ">" || content == "<" {
// We also have to check if the next string is an "="...
if let Some(queue_next) = query.pop_front() {
if queue_next == "=" {
condition = Some(if content == ">" {
QueryComparison::GreaterOrEqual
} else {
QueryComparison::LessOrEqual
});
if let Some(queue_next_next) = query.pop_front() {
value = queue_next_next.parse::<f64>().ok();
} else {
return Err(QueryError("Missing value".into()));
}
} else {
condition = Some(if content == ">" {
QueryComparison::Greater
} else {
QueryComparison::Less
});
value = queue_next.parse::<f64>().ok();
}
} else {
return Err(QueryError("Missing value".into()));
}
}
if let Some(condition) = condition {
if let Some(read_value) = value {
// Note that the values *might* have a unit or need to be parsed differently
// based on the prefix type!
let mut value = read_value;
match prefix_type {
PrefixType::MemBytes
| PrefixType::Rps
| PrefixType::Wps
| PrefixType::TRead
| PrefixType::TWrite => {
// If no unit, assume base.
// Furthermore, base must be PEEKED at initially, and will
// require (likely) prefix_type specific checks
// Lastly, if it *is* a unit, remember to POP!
if let Some(potential_unit) = query.front() {
match potential_unit.to_lowercase().as_str() {
"tb" => {
value *= 1_000_000_000_000.0;
query.pop_front();
}
"tib" => {
value *= 1_099_511_627_776.0;
query.pop_front();
}
"gb" => {
value *= 1_000_000_000.0;
query.pop_front();
}
"gib" => {
value *= 1_073_741_824.0;
query.pop_front();
}
"mb" => {
value *= 1_000_000.0;
query.pop_front();
}
"mib" => {
value *= 1_048_576.0;
query.pop_front();
}
"kb" => {
value *= 1000.0;
query.pop_front();
}
"kib" => {
value *= 1024.0;
query.pop_front();
}
"b" => {
// Just gotta pop.
query.pop_front();
}
_ => {}
}
}
}
_ => {}
}
return Ok(Prefix {
or: None,
regex_prefix: None,
compare_prefix: Some((
prefix_type,
ComparableQuery::Numerical(NumericalQuery {
condition,
value,
}),
)),
});
}
}
}
}
} else {
return Err(QueryError("Missing argument for search prefix".into()));
}
}
} else if inside_quotation {
// Uh oh, it's empty with quotes!
return Err(QueryError("Missing closing quotation".into()));
}
Err(QueryError("Invalid query".into()))
}
let mut split_query = VecDeque::new();
search_query.split_whitespace().for_each(|s| {
// From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses
let mut last = 0;
for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
if last != index {
split_query.push_back(s[last..index].to_owned());
}
split_query.push_back(matched.to_owned());
last = index + matched.len();
}
if last < s.len() {
split_query.push_back(s[last..].to_owned());
}
});
let mut process_filter = process_string_to_filter(&mut split_query)?;
process_filter.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
Ok(process_filter)
}
pub struct Query {
/// Remember, AND > OR, but AND must come after OR when we parse.
pub query: Vec<Or>,
}
impl Query {
pub fn process_regexes(
&mut self, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> Result<()> {
for or in &mut self.query {
or.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
}
Ok(())
}
pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
self.query
.iter()
.all(|ok| ok.check(process, is_using_command))
}
}
impl Debug for Query {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{:?}", self.query))
}
}
#[derive(Default)]
pub struct Or {
pub lhs: And,
pub rhs: Option<Box<And>>,
}
impl Or {
pub fn process_regexes(
&mut self, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> Result<()> {
self.lhs.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
if let Some(rhs) = &mut self.rhs {
rhs.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
}
Ok(())
}
pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
if let Some(rhs) = &self.rhs {
self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command)
} else {
self.lhs.check(process, is_using_command)
}
}
}
impl Debug for Or {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.rhs {
Some(rhs) => f.write_fmt(format_args!("({:?} OR {:?})", self.lhs, rhs)),
None => f.write_fmt(format_args!("{:?}", self.lhs)),
}
}
}
#[derive(Default)]
pub struct And {
pub lhs: Prefix,
pub rhs: Option<Box<Prefix>>,
}
impl And {
pub fn process_regexes(
&mut self, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> Result<()> {
self.lhs.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
if let Some(rhs) = &mut self.rhs {
rhs.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
}
Ok(())
}
pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
if let Some(rhs) = &self.rhs {
self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command)
} else {
self.lhs.check(process, is_using_command)
}
}
}
impl Debug for And {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.rhs {
Some(rhs) => f.write_fmt(format_args!("({:?} AND {:?})", self.lhs, rhs)),
None => f.write_fmt(format_args!("{:?}", self.lhs)),
}
}
}
#[derive(Debug)]
pub enum PrefixType {
Pid,
PCpu,
MemBytes,
PMem,
Rps,
Wps,
TRead,
TWrite,
Name,
State,
User,
Time,
__Nonexhaustive,
}
impl std::str::FromStr for PrefixType {
type Err = BottomError;
fn from_str(s: &str) -> Result<Self> {
use PrefixType::*;
let lower_case = s.to_lowercase();
// Didn't add mem_bytes, total_read, and total_write
// for now as it causes help to be clogged.
match lower_case.as_str() {
"cpu" | "cpu%" => Ok(PCpu),
"mem" | "mem%" => Ok(PMem),
"memb" => Ok(MemBytes),
"read" | "r/s" | "rps" => Ok(Rps),
"write" | "w/s" | "wps" => Ok(Wps),
"tread" | "t.read" => Ok(TRead),
"twrite" | "t.write" => Ok(TWrite),
"pid" => Ok(Pid),
"state" => Ok(State),
"user" => Ok(User),
"time" => Ok(Time),
_ => Ok(Name),
}
}
}
// TODO: This is also jank and could be better represented. Add tests, then clean up!
#[derive(Default)]
pub struct Prefix {
pub or: Option<Box<Or>>,
pub regex_prefix: Option<(PrefixType, StringQuery)>,
pub compare_prefix: Option<(PrefixType, ComparableQuery)>,
}
impl Prefix {
pub fn process_regexes(
&mut self, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> Result<()> {
if let Some(or) = &mut self.or {
return or.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
);
} else if let Some((prefix_type, StringQuery::Value(regex_string))) = &mut self.regex_prefix
{
match prefix_type {
PrefixType::Pid | PrefixType::Name | PrefixType::State | PrefixType::User => {
let escaped_regex: String;
let final_regex_string = &format!(
"{}{}{}{}",
if is_searching_whole_word { "^" } else { "" },
if is_ignoring_case { "(?i)" } else { "" },
if !is_searching_with_regex {
escaped_regex = regex::escape(regex_string);
&escaped_regex
} else {
regex_string
},
if is_searching_whole_word { "$" } else { "" },
);
let taken_pwc = self.regex_prefix.take();
if let Some((taken_pt, _)) = taken_pwc {
self.regex_prefix = Some((
taken_pt,
StringQuery::Regex(regex::Regex::new(final_regex_string)?),
));
}
}
_ => {}
}
}
Ok(())
}
pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
fn matches_condition<I: Into<f64>, J: Into<f64>>(
condition: &QueryComparison, lhs: I, rhs: J,
) -> bool {
let lhs: f64 = lhs.into();
let rhs: f64 = rhs.into();
match condition {
QueryComparison::Equal => (lhs - rhs).abs() < f64::EPSILON,
QueryComparison::Less => lhs < rhs,
QueryComparison::Greater => lhs > rhs,
QueryComparison::LessOrEqual => lhs <= rhs,
QueryComparison::GreaterOrEqual => lhs >= rhs,
}
}
fn matches_duration(condition: &QueryComparison, lhs: Duration, rhs: Duration) -> bool {
match condition {
QueryComparison::Equal => lhs == rhs,
QueryComparison::Less => lhs < rhs,
QueryComparison::Greater => lhs > rhs,
QueryComparison::LessOrEqual => lhs <= rhs,
QueryComparison::GreaterOrEqual => lhs >= rhs,
}
}
if let Some(and) = &self.or {
and.check(process, is_using_command)
} else if let Some((prefix_type, query_content)) = &self.regex_prefix {
if let StringQuery::Regex(r) = query_content {
match prefix_type {
PrefixType::Name => r.is_match(if is_using_command {
process.command.as_str()
} else {
process.name.as_str()
}),
PrefixType::Pid => r.is_match(process.pid.to_string().as_str()),
PrefixType::State => r.is_match(process.process_state.0.as_str()),
PrefixType::User => r.is_match(process.user.as_ref()),
_ => true,
}
} else {
true
}
} else if let Some((prefix_type, comparable_query)) = &self.compare_prefix {
match comparable_query {
ComparableQuery::Numerical(numerical_query) => match prefix_type {
PrefixType::PCpu => matches_condition(
&numerical_query.condition,
process.cpu_usage_percent,
numerical_query.value,
),
PrefixType::PMem => matches_condition(
&numerical_query.condition,
process.mem_usage_percent,
numerical_query.value,
),
PrefixType::MemBytes => matches_condition(
&numerical_query.condition,
process.mem_usage_bytes as f64,
numerical_query.value,
),
PrefixType::Rps => matches_condition(
&numerical_query.condition,
process.read_bytes_per_sec as f64,
numerical_query.value,
),
PrefixType::Wps => matches_condition(
&numerical_query.condition,
process.write_bytes_per_sec as f64,
numerical_query.value,
),
PrefixType::TRead => matches_condition(
&numerical_query.condition,
process.total_read_bytes as f64,
numerical_query.value,
),
PrefixType::TWrite => matches_condition(
&numerical_query.condition,
process.total_write_bytes as f64,
numerical_query.value,
),
_ => true,
},
ComparableQuery::Time(time_query) => match prefix_type {
PrefixType::Time => {
matches_duration(&time_query.condition, process.time, time_query.duration)
}
_ => true,
},
}
} else {
// Somehow we have an empty condition... oh well. Return true.
true
}
}
}
impl Debug for Prefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(or) = &self.or {
f.write_fmt(format_args!("{:?}", or))
} else if let Some(regex_prefix) = &self.regex_prefix {
f.write_fmt(format_args!("{:?}", regex_prefix))
} else if let Some(compare_prefix) = &self.compare_prefix {
f.write_fmt(format_args!("{:?}", compare_prefix))
} else {
f.write_fmt(format_args!(""))
}
}
}
#[derive(Debug)]
pub enum QueryComparison {
Equal,
Less,
Greater,
LessOrEqual,
GreaterOrEqual,
}
#[derive(Debug)]
pub enum StringQuery {
Value(String),
Regex(regex::Regex),
}
#[derive(Debug)]
pub enum ComparableQuery {
Numerical(NumericalQuery),
Time(TimeQuery),
}
#[derive(Debug)]
pub struct NumericalQuery {
pub condition: QueryComparison,
pub value: f64,
}
#[derive(Debug)]
pub struct TimeQuery {
pub condition: QueryComparison,
pub duration: Duration,
}