Skip to main content

vyre_libs/rule/
ast.rs

1// Typed rule conditions and formula trees.
2// TAG RESERVATIONS: PatternExists=0x01, PatternCountGt=0x02,
3// PatternCountGte=0x03, FileSizeLt=0x04, FileSizeLte=0x05,
4// FileSizeGt=0x06, FileSizeGte=0x07, FileSizeEq=0x08, FileSizeNe=0x09,
5// LiteralTrue=0x0A, LiteralFalse=0x0B, RegexMatch=0x0C,
6// SubstringMatch=0x0D, PrefixMatch=0x0E, SuffixMatch=0x0F,
7// RangeMatch=0x10, SetMembership=0x11, 0x12..=0x7F reserved,
8// Opaque=0x80.
9
10use std::sync::Arc;
11
12use crate::rule::builder;
13use vyre_foundation::extension::RuleConditionExt;
14use vyre_foundation::ir::{BufferDecl, Program};
15
16/// A typed rule leaf condition.
17///
18/// `pattern_id` indexes the `rule_bitmaps` and `rule_counts` buffers used by
19/// [`RuleFormula::to_program`]. File-size thresholds are accepted as `u64`;
20/// thresholds above the current scalar IR file-size range are folded to their
21/// mathematically forced result.
22///
23/// # Examples
24///
25/// ```
26/// use vyre_libs::rule::RuleCondition;
27///
28/// let condition = RuleCondition::PatternCountGte {
29///     pattern_id: 7,
30///     threshold: 2,
31/// };
32/// assert!(matches!(condition, RuleCondition::PatternCountGte { .. }));
33/// ```
34#[derive(Debug, Clone)]
35#[non_exhaustive]
36pub enum RuleCondition {
37    /// True when the pattern has any match state.
38    PatternExists {
39        /// Pattern table index.
40        pattern_id: u32,
41    },
42    /// True when the pattern count is strictly greater than `threshold`.
43    PatternCountGt {
44        /// Pattern table index.
45        pattern_id: u32,
46        /// Exclusive lower bound.
47        threshold: u32,
48    },
49    /// True when the pattern count is greater than or equal to `threshold`.
50    PatternCountGte {
51        /// Pattern table index.
52        pattern_id: u32,
53        /// Inclusive lower bound.
54        threshold: u32,
55    },
56    /// True when the file size is less than the threshold.
57    FileSizeLt(u64),
58    /// True when the file size is less than or equal to the threshold.
59    FileSizeLte(u64),
60    /// True when the file size is greater than the threshold.
61    FileSizeGt(u64),
62    /// True when the file size is greater than or equal to the threshold.
63    FileSizeGte(u64),
64    /// True when the file size equals the threshold.
65    FileSizeEq(u64),
66    /// True when the file size does not equal the threshold.
67    FileSizeNe(u64),
68    /// Constant true leaf.
69    LiteralTrue,
70    /// Constant false leaf.
71    LiteralFalse,
72    /// True when text matched by `field` satisfies `pattern`.
73    RegexMatch {
74        /// Source field name.
75        field: Arc<str>,
76        /// Regular expression pattern.
77        pattern: Arc<str>,
78    },
79    /// True when `haystack` contains `needle`.
80    SubstringMatch {
81        /// Source text or field name.
82        haystack: Arc<str>,
83        /// Required substring.
84        needle: Arc<str>,
85    },
86    /// True when `value` starts with `prefix`.
87    PrefixMatch {
88        /// Source text or field name.
89        value: Arc<str>,
90        /// Required prefix.
91        prefix: Arc<str>,
92    },
93    /// True when `value` ends with `suffix`.
94    SuffixMatch {
95        /// Source text or field name.
96        value: Arc<str>,
97        /// Required suffix.
98        suffix: Arc<str>,
99    },
100    /// True when `value` falls inside the inclusive numeric range.
101    RangeMatch {
102        /// Observed value.
103        value: u64,
104        /// Inclusive lower bound.
105        min: u64,
106        /// Inclusive upper bound.
107        max: u64,
108    },
109    /// True when `value` is present in `set`.
110    SetMembership {
111        /// Candidate value.
112        value: Arc<str>,
113        /// Accepted set members.
114        set: smallvec::SmallVec<[Arc<str>; 4]>,
115    },
116    /// True when the value of context field `field` is present in
117    /// `set`. Differs from [`Self::SetMembership`]: this variant
118    /// dereferences `field` against the evaluation context, while
119    /// `SetMembership` compares a static `value` payload.
120    /// Lets a rule express "detector_id is one of …" without
121    /// emulating it via a regex alternation.
122    FieldInSet {
123        /// Context field name to look up (e.g. `"detector_id"`).
124        field: Arc<str>,
125        /// Accepted set members.
126        set: smallvec::SmallVec<[Arc<str>; 4]>,
127    },
128    /// Extension-declared rule condition.
129    ///
130    /// Downstream crates supply an `Arc<dyn RuleConditionExt>` with its
131    /// own evaluator + required-buffer contract. The core rule builder
132    /// rejects opaque conditions because it cannot lower them truthfully;
133    /// extension-aware builders can call [`RuleConditionExt::required_buffers`]
134    /// when wiring the extension to concrete IR.
135    Opaque(Arc<dyn RuleConditionExt>),
136}
137
138impl PartialEq for RuleCondition {
139    fn eq(&self, other: &Self) -> bool {
140        match (self, other) {
141            (Self::PatternExists { pattern_id: a }, Self::PatternExists { pattern_id: b }) => {
142                a == b
143            }
144            (
145                Self::PatternCountGt {
146                    pattern_id: a,
147                    threshold: ta,
148                },
149                Self::PatternCountGt {
150                    pattern_id: b,
151                    threshold: tb,
152                },
153            ) => a == b && ta == tb,
154            (
155                Self::PatternCountGte {
156                    pattern_id: a,
157                    threshold: ta,
158                },
159                Self::PatternCountGte {
160                    pattern_id: b,
161                    threshold: tb,
162                },
163            ) => a == b && ta == tb,
164            (Self::FileSizeLt(a), Self::FileSizeLt(b)) => a == b,
165            (Self::FileSizeLte(a), Self::FileSizeLte(b)) => a == b,
166            (Self::FileSizeGt(a), Self::FileSizeGt(b)) => a == b,
167            (Self::FileSizeGte(a), Self::FileSizeGte(b)) => a == b,
168            (Self::FileSizeEq(a), Self::FileSizeEq(b)) => a == b,
169            (Self::FileSizeNe(a), Self::FileSizeNe(b)) => a == b,
170            (Self::LiteralTrue, Self::LiteralTrue) => true,
171            (Self::LiteralFalse, Self::LiteralFalse) => true,
172            (
173                Self::RegexMatch {
174                    field: af,
175                    pattern: ap,
176                },
177                Self::RegexMatch {
178                    field: bf,
179                    pattern: bp,
180                },
181            ) => af == bf && ap == bp,
182            (
183                Self::SubstringMatch {
184                    haystack: ah,
185                    needle: an,
186                },
187                Self::SubstringMatch {
188                    haystack: bh,
189                    needle: bn,
190                },
191            ) => ah == bh && an == bn,
192            (
193                Self::PrefixMatch {
194                    value: av,
195                    prefix: ap,
196                },
197                Self::PrefixMatch {
198                    value: bv,
199                    prefix: bp,
200                },
201            ) => av == bv && ap == bp,
202            (
203                Self::SuffixMatch {
204                    value: av,
205                    suffix: as_,
206                },
207                Self::SuffixMatch {
208                    value: bv,
209                    suffix: bs,
210                },
211            ) => av == bv && as_ == bs,
212            (
213                Self::RangeMatch {
214                    value: av,
215                    min: amin,
216                    max: amax,
217                },
218                Self::RangeMatch {
219                    value: bv,
220                    min: bmin,
221                    max: bmax,
222                },
223            ) => av == bv && amin == bmin && amax == bmax,
224            (
225                Self::SetMembership {
226                    value: av,
227                    set: aset,
228                },
229                Self::SetMembership {
230                    value: bv,
231                    set: bset,
232                },
233            ) => av == bv && aset == bset,
234            (
235                Self::FieldInSet {
236                    field: af,
237                    set: aset,
238                },
239                Self::FieldInSet {
240                    field: bf,
241                    set: bset,
242                },
243            ) => af == bf && aset == bset,
244            (Self::Opaque(a), Self::Opaque(b)) => a.extension_id() == b.extension_id(),
245            _ => false,
246        }
247    }
248}
249
250impl Eq for RuleCondition {}
251
252impl RuleCondition {
253    /// Return the buffer declarations this condition requires.
254    ///
255    /// Frozen conditions need only the six canonical rule buffers
256    /// (`rule_ids`, `pattern_ids`, `rule_bitmaps`, `rule_counts`,
257    /// `file_size`, `verdicts`). Extension conditions contribute extra
258    /// buffers via [`RuleConditionExt::required_buffers`]  -  callers merge
259    /// the results.
260    #[must_use]
261    pub fn required_extension_buffers(&self) -> Vec<BufferDecl> {
262        match self {
263            Self::Opaque(ext) => ext.required_buffers(),
264            _ => Vec::new(),
265        }
266    }
267}
268
269/// A typed boolean rule formula tree.
270///
271/// Formula nodes compose typed conditions directly. They are not serialized
272/// through an instruction stream and they do not require a runtime reducer.
273///
274/// # Examples
275///
276/// ```
277/// use vyre_libs::rule::{RuleCondition, RuleFormula};
278///
279/// let formula = RuleFormula::and(
280///     RuleFormula::condition(RuleCondition::PatternExists { pattern_id: 0 }),
281///     RuleFormula::not(RuleFormula::condition(RuleCondition::LiteralFalse)),
282/// );
283/// let program = formula.to_program().expect("Fix: formula lowers");
284/// assert!(program.has_buffer("verdicts"));
285/// ```
286#[derive(Debug, Clone, PartialEq, Eq)]
287#[non_exhaustive]
288pub enum RuleFormula {
289    /// Leaf condition.
290    Condition(RuleCondition),
291    /// Logical conjunction.
292    And(Box<RuleFormula>, Box<RuleFormula>),
293    /// Logical disjunction.
294    Or(Box<RuleFormula>, Box<RuleFormula>),
295    /// Logical negation.
296    Not(Box<RuleFormula>),
297}
298
299impl RuleFormula {
300    /// Create a leaf formula.
301    #[must_use]
302    pub fn condition(condition: RuleCondition) -> Self {
303        Self::Condition(condition)
304    }
305
306    /// Create a conjunction.
307    #[must_use]
308    pub fn and(left: Self, right: Self) -> Self {
309        Self::And(Box::new(left), Box::new(right))
310    }
311
312    /// Create a disjunction.
313    #[must_use]
314    pub fn or(left: Self, right: Self) -> Self {
315        Self::Or(Box::new(left), Box::new(right))
316    }
317
318    /// Create a negation.
319    #[must_use]
320    pub fn not_formula(formula: Self) -> Self {
321        Self::Not(Box::new(formula))
322    }
323
324    /// Create a negation.
325    #[must_use]
326    #[allow(clippy::should_implement_trait)]
327    pub fn not(formula: Self) -> Self {
328        Self::not_formula(formula)
329    }
330
331    /// Build a one-rule [`Program`] that stores the formula verdict at index 0.
332    ///
333    /// # Errors
334    ///
335    /// Returns [`builder::RuleBuildError`] when the formula contains a
336    /// condition the core builder cannot lower truthfully.
337    ///
338    /// # Examples
339    ///
340    /// ```
341    /// use vyre_libs::rule::{RuleCondition, RuleFormula};
342    ///
343    /// let program = RuleFormula::condition(RuleCondition::LiteralTrue)
344    ///     .to_program()
345    ///     .expect("Fix: literal rule lowers");
346    /// assert!(program.has_buffer("rule_bitmaps"));
347    /// assert!(program.has_buffer("verdicts"));
348    /// ```
349    #[must_use]
350    pub fn to_program(&self) -> Result<Program, builder::RuleBuildError> {
351        builder::build_rule_program(&[(self.clone(), 0)])
352    }
353
354    /// Try to build a one-rule [`Program`] that stores the formula verdict at
355    /// index 0.
356    ///
357    /// # Errors
358    ///
359    /// Returns [`builder::RuleBuildError`] when the formula contains a
360    /// condition the core builder cannot lower truthfully.
361    pub fn try_to_program(&self) -> Result<Program, builder::RuleBuildError> {
362        self.to_program()
363    }
364}