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}