yazi/yazi-shared/src/condition.rs
Immanuel Tikhonov 22fb9e0d09
fix: keep repeated ! negations in a condition right-associative (#3946)
Co-authored-by: sxyazi <sxyazi@gmail.com>
2026-05-09 22:32:18 +08:00

160 lines
3.3 KiB
Rust

use std::{cmp::Ordering, str::FromStr};
use anyhow::bail;
use serde_with::DeserializeFromStr;
#[derive(Debug, PartialEq, Eq)]
pub enum ConditionOp {
Or,
And,
Not,
// Only used in `build()`
Space,
LeftParen,
RightParen,
Unknown,
// Only used in `eval()`
Term(String),
}
impl ConditionOp {
pub fn new(c: char) -> Self {
match c {
'|' => Self::Or,
'&' => Self::And,
'!' => Self::Not,
'(' => Self::LeftParen,
')' => Self::RightParen,
_ if c.is_ascii_whitespace() => Self::Space,
_ => Self::Unknown,
}
}
fn prec(&self) -> u8 {
match self {
Self::Or => 1,
Self::And => 2,
Self::Not => 3,
_ => 0,
}
}
}
impl PartialOrd for ConditionOp {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
use Ordering::*;
match self.prec().cmp(&other.prec()) {
// Keep repeated `!` right-associative by making `! >= !` false.
Equal if matches!((self, other), (Self::Not, Self::Not)) => None,
ordering => Some(ordering),
}
}
}
#[derive(Debug, DeserializeFromStr)]
pub struct Condition {
ops: Vec<ConditionOp>,
}
impl FromStr for Condition {
type Err = anyhow::Error;
fn from_str(expr: &str) -> Result<Self, Self::Err> {
let cond = Self::build(expr);
if cond.eval(|_| true).is_none() {
bail!("Invalid condition: {expr}");
}
Ok(cond)
}
}
impl Condition {
fn build(expr: &str) -> Self {
let mut stack: Vec<ConditionOp> = vec![];
let mut output: Vec<ConditionOp> = vec![];
let mut chars = expr.chars().peekable();
while let Some(token) = chars.next() {
let op = ConditionOp::new(token);
match op {
ConditionOp::Or | ConditionOp::And | ConditionOp::Not => {
while matches!(stack.last(), Some(last) if last >= &op) {
output.push(stack.pop().unwrap());
}
stack.push(op);
}
ConditionOp::Space => continue,
ConditionOp::LeftParen => stack.push(op),
ConditionOp::RightParen => {
while matches!(stack.last(), Some(last) if last != &ConditionOp::LeftParen) {
output.push(stack.pop().unwrap());
}
stack.pop();
}
ConditionOp::Unknown => {
let mut s = String::from(token);
while matches!(chars.peek(), Some(&c) if ConditionOp::new(c) == op) {
s.push(chars.next().unwrap());
}
output.push(ConditionOp::Term(s));
}
ConditionOp::Term(_) => unreachable!(),
}
}
while let Some(op) = stack.pop() {
output.push(op);
}
Self { ops: output }
}
pub fn eval(&self, f: impl Fn(&str) -> bool) -> Option<bool> {
let mut stack: Vec<bool> = Vec::with_capacity(self.ops.len());
for op in &self.ops {
match op {
ConditionOp::Or => {
let b = stack.pop()? | stack.pop()?;
stack.push(b);
}
ConditionOp::And => {
let b = stack.pop()? & stack.pop()?;
stack.push(b);
}
ConditionOp::Not => {
let b = !stack.pop()?;
stack.push(b);
}
ConditionOp::Term(s) => {
stack.push(f(s));
}
_ => return None,
}
}
if stack.len() == 1 { Some(stack[0]) } else { None }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_condition_not() -> anyhow::Result<()> {
let cond: Condition = "!dir".parse()?;
assert!(!cond.eval(|s| s == "dir").unwrap());
assert!(cond.eval(|_| false).unwrap());
let cond: Condition = "!!dir".parse()?;
assert!(cond.eval(|s| s == "dir").unwrap());
assert!(!cond.eval(|_| false).unwrap());
Ok(())
}
}