Skip to main content

libmagic_rs/evaluator/operators/
comparison.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Comparison operators for magic rule evaluation
5
6use std::cmp::Ordering;
7
8use crate::parser::ast::Value;
9
10/// Compare two values and return their ordering, if comparable
11///
12/// Returns `Some(Ordering)` for same-type comparisons (integers, strings, bytes)
13/// and cross-type integer comparisons (via `i128` coercion). Returns `None` for
14/// incomparable type combinations.
15///
16/// # Examples
17///
18/// ```
19/// use std::cmp::Ordering;
20/// use libmagic_rs::parser::ast::Value;
21/// use libmagic_rs::evaluator::operators::compare_values;
22///
23/// assert_eq!(compare_values(&Value::Uint(5), &Value::Uint(10)), Some(Ordering::Less));
24/// assert_eq!(compare_values(&Value::Int(-1), &Value::Uint(0)), Some(Ordering::Less));
25/// assert_eq!(compare_values(&Value::Uint(42), &Value::Int(42)), Some(Ordering::Equal));
26/// assert_eq!(compare_values(&Value::Uint(1), &Value::String("1".to_string())), None);
27/// ```
28#[must_use]
29pub fn compare_values(left: &Value, right: &Value) -> Option<Ordering> {
30    match (left, right) {
31        (Value::Uint(a), Value::Uint(b)) => Some(a.cmp(b)),
32        (Value::Int(a), Value::Int(b)) => Some(a.cmp(b)),
33        (Value::Uint(a), Value::Int(b)) => Some(i128::from(*a).cmp(&i128::from(*b))),
34        (Value::Int(a), Value::Uint(b)) => Some(i128::from(*a).cmp(&i128::from(*b))),
35        (Value::Float(a), Value::Float(b)) => a.partial_cmp(b),
36        (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
37        (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)),
38        // Cross-type byte-sequence ordering: parser produces `Value::Bytes`
39        // for backslash-escape patterns like `\177ELF` while
40        // `read_string_exact` returns `Value::String`. Both must compare
41        // by underlying byte sequence so that `<`/`>`/`<=`/`>=` are
42        // consistent with the cross-type policy added to `apply_equal`.
43        // Without this, the trichotomy invariant breaks: two values
44        // could be neither less, equal, nor greater. See GOTCHAS S2.3.
45        (Value::String(s), Value::Bytes(b)) => Some(s.as_bytes().cmp(b.as_slice())),
46        (Value::Bytes(b), Value::String(s)) => Some(b.as_slice().cmp(s.as_bytes())),
47        _ => None,
48    }
49}
50
51/// Apply less-than comparison between two values
52///
53/// # Examples
54///
55/// ```
56/// use libmagic_rs::parser::ast::Value;
57/// use libmagic_rs::evaluator::operators::apply_less_than;
58///
59/// assert!(apply_less_than(&Value::Uint(5), &Value::Uint(10)));
60/// assert!(!apply_less_than(&Value::Uint(10), &Value::Uint(10)));
61/// assert!(apply_less_than(&Value::Int(-1), &Value::Uint(0)));
62/// ```
63#[must_use]
64pub fn apply_less_than(left: &Value, right: &Value) -> bool {
65    compare_values(left, right) == Some(Ordering::Less)
66}
67
68/// Apply greater-than comparison between two values
69///
70/// # Examples
71///
72/// ```
73/// use libmagic_rs::parser::ast::Value;
74/// use libmagic_rs::evaluator::operators::apply_greater_than;
75///
76/// assert!(apply_greater_than(&Value::Uint(10), &Value::Uint(5)));
77/// assert!(!apply_greater_than(&Value::Uint(10), &Value::Uint(10)));
78/// assert!(apply_greater_than(&Value::Uint(0), &Value::Int(-1)));
79/// ```
80#[must_use]
81pub fn apply_greater_than(left: &Value, right: &Value) -> bool {
82    compare_values(left, right) == Some(Ordering::Greater)
83}
84
85/// Apply less-than-or-equal comparison between two values
86///
87/// # Examples
88///
89/// ```
90/// use libmagic_rs::parser::ast::Value;
91/// use libmagic_rs::evaluator::operators::apply_less_equal;
92///
93/// assert!(apply_less_equal(&Value::Uint(10), &Value::Uint(10)));
94/// assert!(apply_less_equal(&Value::Uint(5), &Value::Uint(10)));
95/// assert!(!apply_less_equal(&Value::Uint(10), &Value::Uint(5)));
96/// ```
97#[must_use]
98pub fn apply_less_equal(left: &Value, right: &Value) -> bool {
99    matches!(
100        compare_values(left, right),
101        Some(Ordering::Less | Ordering::Equal)
102    )
103}
104
105/// Apply greater-than-or-equal comparison between two values
106///
107/// # Examples
108///
109/// ```
110/// use libmagic_rs::parser::ast::Value;
111/// use libmagic_rs::evaluator::operators::apply_greater_equal;
112///
113/// assert!(apply_greater_equal(&Value::Uint(10), &Value::Uint(10)));
114/// assert!(apply_greater_equal(&Value::Uint(10), &Value::Uint(5)));
115/// assert!(!apply_greater_equal(&Value::Uint(5), &Value::Uint(10)));
116/// ```
117#[must_use]
118pub fn apply_greater_equal(left: &Value, right: &Value) -> bool {
119    matches!(
120        compare_values(left, right),
121        Some(Ordering::Greater | Ordering::Equal)
122    )
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_compare_values_ordering() {
131        use std::cmp::Ordering::*;
132
133        // Same-type integer comparisons
134        assert_eq!(
135            compare_values(&Value::Uint(5), &Value::Uint(10)),
136            Some(Less)
137        );
138        assert_eq!(
139            compare_values(&Value::Uint(10), &Value::Uint(10)),
140            Some(Equal)
141        );
142        assert_eq!(
143            compare_values(&Value::Uint(10), &Value::Uint(5)),
144            Some(Greater)
145        );
146        assert_eq!(
147            compare_values(&Value::Int(-10), &Value::Int(-5)),
148            Some(Less)
149        );
150        assert_eq!(
151            compare_values(&Value::Int(i64::MIN), &Value::Int(0)),
152            Some(Less)
153        );
154
155        // Cross-type integer comparisons via i128
156        assert_eq!(compare_values(&Value::Int(-1), &Value::Uint(0)), Some(Less));
157        assert_eq!(
158            compare_values(&Value::Uint(42), &Value::Int(42)),
159            Some(Equal)
160        );
161        assert_eq!(
162            compare_values(&Value::Uint(u64::MAX), &Value::Int(-1)),
163            Some(Greater)
164        );
165
166        // String comparisons
167        assert_eq!(
168            compare_values(&Value::String("abc".into()), &Value::String("abd".into())),
169            Some(Less)
170        );
171        assert_eq!(
172            compare_values(&Value::String("abc".into()), &Value::String("abc".into())),
173            Some(Equal)
174        );
175
176        // Bytes comparisons (lexicographic, including different lengths)
177        assert_eq!(
178            compare_values(&Value::Bytes(vec![1]), &Value::Bytes(vec![2])),
179            Some(Less)
180        );
181        assert_eq!(
182            compare_values(&Value::Bytes(vec![1]), &Value::Bytes(vec![1])),
183            Some(Equal)
184        );
185        assert_eq!(
186            compare_values(&Value::Bytes(vec![1]), &Value::Bytes(vec![1, 2])),
187            Some(Less)
188        );
189        assert_eq!(
190            compare_values(&Value::Bytes(vec![]), &Value::Bytes(vec![1])),
191            Some(Less)
192        );
193
194        // Incomparable types return None
195        assert_eq!(
196            compare_values(&Value::Uint(1), &Value::String("1".into())),
197            None
198        );
199        assert_eq!(compare_values(&Value::Int(1), &Value::Bytes(vec![1])), None);
200    }
201
202    #[test]
203    fn test_compare_values_float_ordering() {
204        use std::cmp::Ordering::*;
205
206        assert_eq!(
207            compare_values(&Value::Float(1.0), &Value::Float(2.0)),
208            Some(Less)
209        );
210        assert_eq!(
211            compare_values(&Value::Float(2.0), &Value::Float(2.0)),
212            Some(Equal)
213        );
214        assert_eq!(
215            compare_values(&Value::Float(3.0), &Value::Float(2.0)),
216            Some(Greater)
217        );
218        assert_eq!(
219            compare_values(&Value::Float(-1.0), &Value::Float(1.0)),
220            Some(Less)
221        );
222
223        // Infinity ordering
224        assert_eq!(
225            compare_values(&Value::Float(1.0), &Value::Float(f64::INFINITY)),
226            Some(Less)
227        );
228        assert_eq!(
229            compare_values(&Value::Float(f64::NEG_INFINITY), &Value::Float(1.0)),
230            Some(Less)
231        );
232        assert_eq!(
233            compare_values(&Value::Float(f64::INFINITY), &Value::Float(f64::INFINITY)),
234            Some(Equal)
235        );
236
237        // NaN is not comparable
238        assert_eq!(
239            compare_values(&Value::Float(f64::NAN), &Value::Float(1.0)),
240            None
241        );
242        assert_eq!(
243            compare_values(&Value::Float(1.0), &Value::Float(f64::NAN)),
244            None
245        );
246        assert_eq!(
247            compare_values(&Value::Float(f64::NAN), &Value::Float(f64::NAN)),
248            None
249        );
250
251        // Float vs non-float is incomparable
252        assert_eq!(compare_values(&Value::Float(1.0), &Value::Uint(1)), None);
253        assert_eq!(compare_values(&Value::Int(1), &Value::Float(1.0)), None);
254    }
255
256    #[test]
257    fn test_comparison_operators_float() {
258        // Direct partial_cmp semantics for ordering operators
259        assert!(apply_less_than(&Value::Float(1.0), &Value::Float(2.0)));
260        assert!(!apply_less_than(&Value::Float(2.0), &Value::Float(2.0)));
261        assert!(apply_greater_than(&Value::Float(3.0), &Value::Float(2.0)));
262        assert!(!apply_greater_than(&Value::Float(2.0), &Value::Float(2.0)));
263        assert!(apply_less_equal(&Value::Float(2.0), &Value::Float(2.0)));
264        assert!(apply_less_equal(&Value::Float(1.0), &Value::Float(2.0)));
265        assert!(apply_greater_equal(&Value::Float(2.0), &Value::Float(2.0)));
266        assert!(apply_greater_equal(&Value::Float(3.0), &Value::Float(2.0)));
267
268        // NaN comparisons all return false
269        assert!(!apply_less_than(
270            &Value::Float(f64::NAN),
271            &Value::Float(1.0)
272        ));
273        assert!(!apply_greater_than(
274            &Value::Float(f64::NAN),
275            &Value::Float(1.0)
276        ));
277        assert!(!apply_less_equal(
278            &Value::Float(f64::NAN),
279            &Value::Float(1.0)
280        ));
281        assert!(!apply_greater_equal(
282            &Value::Float(f64::NAN),
283            &Value::Float(1.0)
284        ));
285    }
286
287    #[test]
288    fn test_comparison_operators_consistency() {
289        // Verify all four comparison functions agree with compare_values
290        let pairs = vec![
291            (Value::Uint(5), Value::Uint(10)),
292            (Value::Uint(10), Value::Uint(10)),
293            (Value::Uint(10), Value::Uint(5)),
294            (Value::Int(-10), Value::Int(-5)),
295            (Value::Int(-1), Value::Uint(0)),
296            (Value::Uint(u64::MAX), Value::Int(-1)),
297            (Value::String("abc".into()), Value::String("abd".into())),
298            (Value::Bytes(vec![1, 2]), Value::Bytes(vec![1, 3])),
299            (Value::Bytes(vec![1]), Value::Bytes(vec![1, 2])),
300            (Value::Uint(1), Value::String("1".into())), // incomparable
301        ];
302
303        for (left, right) in &pairs {
304            let ord = compare_values(left, right);
305            assert_eq!(
306                apply_less_than(left, right),
307                ord == Some(Ordering::Less),
308                "< for {left:?}, {right:?}"
309            );
310            assert_eq!(
311                apply_greater_than(left, right),
312                ord == Some(Ordering::Greater),
313                "> for {left:?}, {right:?}"
314            );
315            assert_eq!(
316                apply_less_equal(left, right),
317                matches!(ord, Some(Ordering::Less | Ordering::Equal)),
318                "<= for {left:?}, {right:?}"
319            );
320            assert_eq!(
321                apply_greater_equal(left, right),
322                matches!(ord, Some(Ordering::Greater | Ordering::Equal)),
323                ">= for {left:?}, {right:?}"
324            );
325        }
326    }
327
328    /// Cross-type `Value::String` <-> `Value::Bytes` comparisons must
329    /// honor the same byte-sequence ordering that `apply_equal` uses
330    /// for cross-type equality. Without this, the trichotomy invariant
331    /// breaks: two byte-equal values would compare as `==` but neither
332    /// `<` nor `>` would be true (and a byte-unequal pair would have
333    /// `<` and `>` both false). magic(5) rules using `>` or `<` against
334    /// a `\177ELF`-style `Value::Bytes` literal vs a `Value::String`
335    /// read would silently never fire.
336    #[test]
337    fn cross_type_string_bytes_ordering_is_byte_sequence() {
338        // Equal bytes -> Equal regardless of variant order
339        assert_eq!(
340            compare_values(
341                &Value::String("abc".to_string()),
342                &Value::Bytes(b"abc".to_vec())
343            ),
344            Some(Ordering::Equal)
345        );
346        assert_eq!(
347            compare_values(
348                &Value::Bytes(b"abc".to_vec()),
349                &Value::String("abc".to_string())
350            ),
351            Some(Ordering::Equal)
352        );
353
354        // Less / Greater follow byte-lex ordering
355        assert_eq!(
356            compare_values(
357                &Value::Bytes(b"a".to_vec()),
358                &Value::String("b".to_string())
359            ),
360            Some(Ordering::Less)
361        );
362        assert_eq!(
363            compare_values(
364                &Value::String("z".to_string()),
365                &Value::Bytes(b"a".to_vec())
366            ),
367            Some(Ordering::Greater)
368        );
369
370        // Trichotomy: for any cross-type pair, exactly one of <, ==, >
371        // is true via the apply_* helpers.
372        let pairs: &[(Value, Value)] = &[
373            (Value::String("abc".into()), Value::Bytes(b"abc".to_vec())),
374            (Value::String("abc".into()), Value::Bytes(b"abd".to_vec())),
375            (Value::Bytes(b"abc".to_vec()), Value::String("abb".into())),
376        ];
377        for (l, r) in pairs {
378            let lt = apply_less_than(l, r);
379            let eq = compare_values(l, r) == Some(Ordering::Equal);
380            let gt = apply_greater_than(l, r);
381            assert_eq!(
382                u8::from(lt) + u8::from(eq) + u8::from(gt),
383                1,
384                "trichotomy broken for {l:?} vs {r:?}: lt={lt} eq={eq} gt={gt}"
385            );
386        }
387    }
388}