#[derive(Debug, Clone, PartialEq)]
pub enum QueryType {
Term(String),
Or(Vec<Box<QueryType>>),
And(Vec<Box<QueryType>>),
Not(Box<QueryType>),
}
const MAX_LOGICAL_QUERY_LEN: usize = 8 * 1024;
const MAX_LOGICAL_QUERY_DEPTH: usize = 64;
const MAX_LOGICAL_QUERY_NODES: usize = 1_024;
const MAX_LOGICAL_QUERY_BRANCHES: usize = 512;
impl QueryType {
pub fn parse(query: &str) -> Self {
let query = query.trim();
if query.is_empty() {
return QueryType::Or(vec![]);
}
Self::parse_or_expression(query, 0)
}
pub fn try_parse(query: &str) -> Result<Self, String> {
validate_query_input(query)?;
let query = Self::parse(query);
query.validate_complexity()?;
Ok(query)
}
pub fn may_materialize_not_complement(&self) -> bool {
may_materialize_not_complement(self, false)
}
fn validate_complexity(&self) -> Result<(), String> {
let mut stats = QueryStats::default();
validate_ast(self, 0, &mut stats)
}
fn parse_or_expression(query: &str, depth: usize) -> Self {
let parts: Vec<&str> = Self::split_top_level(query, " OR ");
if parts.len() == 1 {
return Self::parse_and_expression(parts[0], depth);
}
let subqueries: Vec<Box<QueryType>> = parts
.into_iter()
.map(|p| Box::new(Self::parse_and_expression(p, depth)))
.collect();
QueryType::Or(subqueries)
}
fn parse_and_expression(query: &str, depth: usize) -> Self {
let parts: Vec<&str> = Self::split_top_level(query, " AND ");
if parts.len() == 1 {
return Self::parse_not_expression(parts[0], depth);
}
let subqueries: Vec<Box<QueryType>> = parts
.into_iter()
.map(|p| Box::new(Self::parse_not_expression(p, depth)))
.collect();
QueryType::And(subqueries)
}
fn parse_not_expression(query: &str, depth: usize) -> Self {
let query = query.trim();
if let Some(stripped) = query.strip_prefix("NOT ") {
return QueryType::Not(Box::new(Self::parse_term(stripped, depth)));
}
Self::parse_term(query, depth)
}
fn parse_term(query: &str, depth: usize) -> Self {
let query = query.trim();
if depth < MAX_LOGICAL_QUERY_DEPTH {
if let Some(stripped) = query.strip_prefix('(') {
if stripped.ends_with(')') && Self::is_balanced_parentheses(query) {
return Self::parse_or_expression(&stripped[..stripped.len() - 1], depth + 1);
} else {
return Self::parse_or_expression(stripped, depth + 1);
}
} else if query.ends_with(')') {
return Self::parse_or_expression(query.trim_end_matches(')'), depth + 1);
}
}
let terms: Vec<&str> = query.split_whitespace().collect();
if terms.len() > 1 {
let subqueries: Vec<Box<QueryType>> = terms
.into_iter()
.map(|t| Box::new(QueryType::Term(t.to_lowercase())))
.collect();
return QueryType::Or(subqueries);
}
if !query.is_empty() {
return QueryType::Term(query.to_lowercase());
}
QueryType::Or(vec![])
}
fn is_balanced_parentheses(s: &str) -> bool {
let mut count = 0;
for c in s.chars() {
if c == '(' {
count += 1;
} else if c == ')' {
count -= 1;
if count < 0 {
return false;
}
}
}
count == 0
}
fn split_top_level<'a>(s: &'a str, delimiter: &str) -> Vec<&'a str> {
debug_assert!(delimiter.is_ascii());
let mut result = Vec::new();
let mut start = 0;
let mut paren_count: u32 = 0;
let bytes = s.as_bytes();
let delim_bytes = delimiter.as_bytes();
let delim_len = delim_bytes.len();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'(' => {
paren_count += 1;
i += 1;
}
b')' => {
paren_count = paren_count.saturating_sub(1);
i += 1;
}
_ if paren_count == 0
&& i + delim_len <= bytes.len()
&& bytes[i..i + delim_len] == *delim_bytes =>
{
result.push(s[start..i].trim());
i += delim_len;
start = i;
}
_ => {
i += 1;
}
}
}
result.push(s[start..].trim());
result
}
}
fn may_materialize_not_complement(query: &QueryType, negated_not: bool) -> bool {
match query {
QueryType::Term(_) => false,
QueryType::Not(subquery) => !negated_not || may_materialize_not_complement(subquery, false),
QueryType::Or(subqueries) => subqueries
.iter()
.any(|query| may_materialize_not_complement(query, false)),
QueryType::And(subqueries) => {
if subqueries.is_empty() {
return false;
}
if subqueries.len() == 1 {
return may_materialize_not_complement(&subqueries[0], false);
}
let has_positive = subqueries
.iter()
.any(|query| !matches!(query.as_ref(), QueryType::Not(_)));
if has_positive {
return subqueries
.iter()
.filter(|query| !matches!(query.as_ref(), QueryType::Not(_)))
.any(|query| may_materialize_not_complement(query, false))
|| subqueries
.iter()
.filter(|query| matches!(query.as_ref(), QueryType::Not(_)))
.any(|query| may_materialize_not_complement(query, true));
}
let mut negatives = subqueries.iter();
if let Some(first) = negatives.next()
&& may_materialize_not_complement(first, false)
{
return true;
}
negatives.any(|query| may_materialize_not_complement(query, true))
}
}
}
#[derive(Default)]
struct QueryStats {
nodes: usize,
branches: usize,
}
fn validate_query_input(query: &str) -> Result<(), String> {
if query.len() > MAX_LOGICAL_QUERY_LEN {
return Err(format!(
"logical query length {} exceeds maximum {MAX_LOGICAL_QUERY_LEN}",
query.len()
));
}
let mut depth = 0usize;
let mut max_depth = 0usize;
let mut unmatched_close = 0usize;
for ch in query.chars() {
match ch {
'(' => {
depth = depth.saturating_add(1);
max_depth = max_depth.max(depth);
if max_depth > MAX_LOGICAL_QUERY_DEPTH {
return Err(format!(
"logical query parenthesis depth exceeds maximum {MAX_LOGICAL_QUERY_DEPTH}"
));
}
}
')' => {
if depth == 0 {
unmatched_close += 1;
if unmatched_close > MAX_LOGICAL_QUERY_DEPTH {
return Err(format!(
"logical query unmatched closing parenthesis count exceeds maximum {MAX_LOGICAL_QUERY_DEPTH}"
));
}
} else {
depth -= 1;
}
}
_ => {}
}
}
Ok(())
}
fn validate_ast(query: &QueryType, depth: usize, stats: &mut QueryStats) -> Result<(), String> {
if depth > MAX_LOGICAL_QUERY_DEPTH {
return Err(format!(
"logical query AST depth exceeds maximum {MAX_LOGICAL_QUERY_DEPTH}"
));
}
stats.nodes = stats.nodes.saturating_add(1);
if stats.nodes > MAX_LOGICAL_QUERY_NODES {
return Err(format!(
"logical query AST node count exceeds maximum {MAX_LOGICAL_QUERY_NODES}"
));
}
match query {
QueryType::Term(_) => Ok(()),
QueryType::Not(query) => validate_ast(query, depth + 1, stats),
QueryType::Or(queries) | QueryType::And(queries) => {
stats.branches = stats.branches.saturating_add(queries.len());
if stats.branches > MAX_LOGICAL_QUERY_BRANCHES {
return Err(format!(
"logical query AST branch count exceeds maximum {MAX_LOGICAL_QUERY_BRANCHES}"
));
}
for query in queries {
validate_ast(query, depth + 1, stats)?;
}
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_term() {
assert_eq!(
QueryType::parse("hello"),
QueryType::Term("hello".to_string())
);
}
#[test]
fn test_and_query() {
assert_eq!(
QueryType::parse("hello AND world"),
QueryType::And(vec![
Box::new(QueryType::Term("hello".to_string())),
Box::new(QueryType::Term("world".to_string()))
])
);
}
#[test]
fn test_or_query() {
assert_eq!(
QueryType::parse("hello OR world"),
QueryType::Or(vec![
Box::new(QueryType::Term("hello".to_string())),
Box::new(QueryType::Term("world".to_string()))
])
);
}
#[test]
fn test_not_query() {
assert_eq!(
QueryType::parse("NOT hello"),
QueryType::Not(Box::new(QueryType::Term("hello".to_string())))
);
}
#[test]
fn test_complex_query() {
assert_eq!(
QueryType::parse("(hello AND world) OR (rust AND NOT java)"),
QueryType::Or(vec![
Box::new(QueryType::And(vec![
Box::new(QueryType::Term("hello".to_string())),
Box::new(QueryType::Term("world".to_string()))
])),
Box::new(QueryType::And(vec![
Box::new(QueryType::Term("rust".to_string())),
Box::new(QueryType::Not(Box::new(QueryType::Term(
"java".to_string()
))))
]))
])
);
}
#[test]
fn test_unbalanced_parentheses() {
assert_eq!(
QueryType::parse("(hello AND world"),
QueryType::And(vec![
Box::new(QueryType::Term("hello".to_string())),
Box::new(QueryType::Term("world".to_string()))
])
);
assert_eq!(
QueryType::parse("hello AND world)"),
QueryType::And(vec![
Box::new(QueryType::Term("hello".to_string())),
Box::new(QueryType::Term("world".to_string()))
])
);
assert_eq!(
QueryType::parse("(hello AND (world OR rust)"),
QueryType::And(vec![
Box::new(QueryType::Term("hello".to_string())),
Box::new(QueryType::Or(vec![
Box::new(QueryType::Term("world".to_string())),
Box::new(QueryType::Term("rust".to_string()))
]))
])
);
}
#[test]
fn test_multibyte_utf8_query() {
assert_eq!(
QueryType::parse("巨蟹"),
QueryType::Term("巨蟹".to_string())
);
assert_eq!(
QueryType::parse("巨蟹 AND rust"),
QueryType::And(vec![
Box::new(QueryType::Term("巨蟹".to_string())),
Box::new(QueryType::Term("rust".to_string()))
])
);
assert_eq!(
QueryType::parse("巨蟹 OR 天蝎"),
QueryType::Or(vec![
Box::new(QueryType::Term("巨蟹".to_string())),
Box::new(QueryType::Term("天蝎".to_string()))
])
);
assert_eq!(
QueryType::parse("(巨蟹 AND 座) OR 天蝎"),
QueryType::Or(vec![
Box::new(QueryType::And(vec![
Box::new(QueryType::Term("巨蟹".to_string())),
Box::new(QueryType::Term("座".to_string()))
])),
Box::new(QueryType::Term("天蝎".to_string()))
])
);
assert_eq!(
QueryType::parse("NOT 巨蟹"),
QueryType::Not(Box::new(QueryType::Term("巨蟹".to_string())))
);
assert_eq!(
QueryType::parse("巨蟹 天蝎 双鱼"),
QueryType::Or(vec![
Box::new(QueryType::Term("巨蟹".to_string())),
Box::new(QueryType::Term("天蝎".to_string())),
Box::new(QueryType::Term("双鱼".to_string()))
])
);
}
#[test]
fn try_parse_rejects_excessive_parenthesis_depth() {
let query = format!("{}hello{}", "(".repeat(MAX_LOGICAL_QUERY_DEPTH + 1), ")");
assert!(QueryType::try_parse(&query).is_err());
}
#[test]
fn try_parse_rejects_close_parenthesis_flood() {
let query = format!("x{}", ")".repeat(MAX_LOGICAL_QUERY_DEPTH + 1));
assert!(QueryType::try_parse(&query).is_err());
let query = format!("x{}", ")".repeat(MAX_LOGICAL_QUERY_DEPTH));
assert_eq!(
QueryType::try_parse(&query).unwrap(),
QueryType::Term("x".to_string())
);
}
#[test]
fn parse_survives_parenthesis_floods_without_stack_overflow() {
let query = format!("x{}", ")".repeat(8190));
assert_eq!(QueryType::parse(&query), QueryType::Term("x".to_string()));
let query = format!("x{}", ")".repeat(1_000_000));
assert_eq!(QueryType::parse(&query), QueryType::Term("x".to_string()));
let query = format!("{}hello", "(".repeat(1_000_000));
let _ = QueryType::parse(&query);
let query = format!("x{}", " )".repeat(100_000));
let _ = QueryType::parse(&query);
let query = ")(".repeat(500_000);
let _ = QueryType::parse(&query);
}
#[test]
fn parse_close_paren_stripping_matches_old_semantics() {
assert_eq!(
QueryType::parse("hello AND world))"),
QueryType::And(vec![
Box::new(QueryType::Term("hello".to_string())),
Box::new(QueryType::Term("world".to_string()))
])
);
assert_eq!(
QueryType::parse("a) OR b)"),
QueryType::Or(vec![
Box::new(QueryType::Term("a".to_string())),
Box::new(QueryType::Term("b".to_string()))
])
);
assert_eq!(
QueryType::parse("NOT NOT a))"),
QueryType::Not(Box::new(QueryType::Not(Box::new(QueryType::Term(
"a".to_string()
)))))
);
assert_eq!(QueryType::parse(")))"), QueryType::Or(vec![]));
}
#[test]
fn not_complement_detection_distinguishes_and_not_filter() {
let query = QueryType::try_parse("hello AND NOT world").unwrap();
assert!(!query.may_materialize_not_complement());
let query = QueryType::try_parse("hello OR NOT world").unwrap();
assert!(query.may_materialize_not_complement());
let query = QueryType::try_parse("hello AND NOT (world AND NOT rust)").unwrap();
assert!(!query.may_materialize_not_complement());
let query = QueryType::try_parse("hello AND NOT (world OR NOT rust)").unwrap();
assert!(query.may_materialize_not_complement());
let query = QueryType::try_parse("hello AND NOT (NOT world)").unwrap();
assert!(query.may_materialize_not_complement());
}
}