diff --git a/AGENTS.md b/AGENTS.md index cd26967a..aed9486b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -208,7 +208,7 @@ cargo test --doc # Test documentation examples ### Currently Implemented (v0.1.0) - **Offsets**: Absolute and from-end specifications (indirect and relative are parsed but not yet evaluated) -- **Types**: `byte`, `short`, `long`, `quad`, `string` with endianness support; unsigned variants `ubyte`, `ushort`/`ubeshort`/`uleshort`, `ulong`/`ubelong`/`ulelong`, `uquad`/`ubequad`/`ulequad`; types are signed by default (libmagic-compatible) +- **Types**: `byte`, `short`, `long`, `quad`, `float`, `double`, `string` with endianness support; unsigned variants `ubyte`, `ushort`/`ubeshort`/`uleshort`, `ulong`/`ubelong`/`ulelong`, `uquad`/`ubequad`/`ulequad`; float/double endian variants `befloat`/`lefloat`, `bedouble`/`ledouble`; types are signed by default (libmagic-compatible) - **Operators**: `=` (equal), `!=` (not equal), `<` (less than), `>` (greater than), `<=` (less equal), `>=` (greater equal), `&` (bitwise AND with optional mask), `^` (bitwise XOR), `~` (bitwise NOT), `x` (any value) - **Nested Rules**: Hierarchical rule evaluation with proper indentation - **String Matching**: Exact string matching with null-termination @@ -240,7 +240,6 @@ impl BinaryRegex for regex::bytes::Regex { - No regex/search pattern matching - 64-bit integer types: `quad`/`uquad`, `bequad`/`ubequad`, `lequad`/`ulequad` are implemented; `qquad` (128-bit) is not yet supported -- No floating-point types (float, double, befloat, lefloat) - No date/time types (date, qdate, ldate, qldate) - String evaluation reads until first NUL or end-of-buffer by default; `max_length: Some(_)` is supported internally but no dedicated fixed-length string parser syntax exists yet @@ -477,7 +476,7 @@ CI must pass before merge. Mergify merge protections enforce these checks. Bot P 1. **MVP (v0.1.0)** - CURRENT: Basic parsing and evaluation with byte/short/long/quad/string types, equality and bitwise AND operators, built-in rules for 10 common formats 2. **Enhanced Features (v0.2)**: Comparison operators (`>`, `<`), indirect offset improvements, strength-based rule ordering -3. **Advanced Types (v0.3)**: Regex type, floating-point types, search patterns +3. **Advanced Types (v0.3)**: Regex type, search patterns 4. **Full Compatibility (v0.4)**: Complete libmagic syntax support, all special directives, named tests 5. **Production Ready (v1.0)**: Stable API, complete documentation, 95%+ compatibility with GNU file diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 1f30af12..a61d4453 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -302,6 +302,8 @@ use libmagic_rs::TypeKind; | `Short { endian, signed }` | 16-bit integer | | `Long { endian, signed }` | 32-bit integer | | `Quad { endian, signed }` | 64-bit integer | +| `Float { endian }` | 32-bit IEEE 754 floating-point | +| `Double { endian }` | 64-bit IEEE 754 floating-point | | `String { max_length }` | String data | ##### 64-bit Integer Types @@ -319,6 +321,26 @@ The `Quad` variant supports six endian-signedness combinations: **Version Note:** In v0.2.0, the `Byte` variant changed from a unit variant to a struct variant with a `signed` field. +##### 32-bit Floating-Point Types + +The `Float` variant supports three endian variants: + +| Type Specifier | Endianness | Description | +| -------------- | ---------- | ----------------------------------- | +| `float` | Native | Native-endian 32-bit IEEE 754 float | +| `lefloat` | Little | Little-endian 32-bit IEEE 754 float | +| `befloat` | Big | Big-endian 32-bit IEEE 754 float | + +##### 64-bit Floating-Point Types + +The `Double` variant supports three endian variants: + +| Type Specifier | Endianness | Description | +| -------------- | ---------- | ------------------------------------ | +| `double` | Native | Native-endian 64-bit IEEE 754 double | +| `ledouble` | Little | Little-endian 64-bit IEEE 754 double | +| `bedouble` | Big | Big-endian 64-bit IEEE 754 double | + #### Operator Comparison operators. @@ -343,6 +365,12 @@ use libmagic_rs::Operator; **Version Note:** The comparison operators `LessThan`, `GreaterThan`, `LessEqual`, and `GreaterEqual` were added in v0.2.0. +##### Floating-Point Comparison Semantics + +Equality operators (`Equal`, `NotEqual`) use epsilon-aware comparison for `Value::Float` operands: two floats are considered equal when `|a - b| <= f64::EPSILON`. NaN is never equal to anything (including itself), and infinities are equal only to the same-signed infinity. + +Ordering operators (`LessThan`, `GreaterThan`, `LessEqual`, `GreaterEqual`) use IEEE 754 `partial_cmp` semantics. All NaN comparisons return `false` (NaN is not comparable to any value). + #### Value Value types for matching. @@ -351,12 +379,15 @@ Value types for matching. use libmagic_rs::Value; ``` -| Variant | Description | -| ---------------- | ---------------- | -| `Uint(u64)` | Unsigned integer | -| `Int(i64)` | Signed integer | -| `Bytes(Vec)` | Byte sequence | -| `String(String)` | String value | +| Variant | Description | +| ---------------- | --------------------------- | +| `Uint(u64)` | Unsigned integer | +| `Int(i64)` | Signed integer | +| `Float(f64)` | 64-bit floating-point value | +| `Bytes(Vec)` | Byte sequence | +| `String(String)` | String value | + +**Note:** `Value` implements `PartialEq` but not `Eq` due to IEEE 754 NaN semantics (NaN is not equal to itself). #### Endianness diff --git a/docs/src/evaluator.md b/docs/src/evaluator.md index 5e3a1e07..b9aaace7 100644 --- a/docs/src/evaluator.md +++ b/docs/src/evaluator.md @@ -32,6 +32,7 @@ The evaluator module separates public interface from implementation: - **`evaluator/types/`** - Type reading and coercion (organized as submodules as of v0.4.2) - **`types/mod.rs`** - Public API surface: `read_typed_value`, `coerce_value_to_type`, re-exports type functions - **`types/numeric.rs`** - Numeric type handling: `read_byte`, `read_short`, `read_long`, `read_quad` with endianness and signedness support + - **`types/float.rs`** - Floating-point type handling: `read_float` (32-bit IEEE 754), `read_double` (64-bit IEEE 754) with endianness support - **`types/string.rs`** - String type handling: `read_string` with null-termination and UTF-8 conversion - **`types/tests.rs`** - Module tests - **`evaluator/strength.rs`** - Rule strength calculation @@ -85,7 +86,7 @@ pub struct RuleMatch { } ``` -The `Value` type is from `parser::ast::Value` and represents the actual matched content according to the rule's type specification. +The `Value` type is from `parser::ast::Value` and represents the actual matched content according to the rule's type specification. Note that `Value` implements only `PartialEq` (not `Eq`) due to floating-point NaN semantics. ### Offset Resolution (`evaluator/offset.rs`) @@ -105,12 +106,14 @@ pub fn resolve_offset( ### Type Reading (`evaluator/types/`) -Interprets bytes according to type specifications. The types module is organized into submodules for numeric and string type handling (refactored from a single file in v0.4.2): +Interprets bytes according to type specifications. The types module is organized into submodules for numeric, floating-point, and string type handling (refactored from a single file in v0.4.2): - **Byte**: Single byte values (signed or unsigned) - **Short**: 16-bit integers with endianness - **Long**: 32-bit integers with endianness - **Quad**: 64-bit integers with endianness +- **Float**: 32-bit IEEE 754 floating-point with endianness (native, big-endian `befloat`, little-endian `lefloat`) +- **Double**: 64-bit IEEE 754 floating-point with endianness (native, big-endian `bedouble`, little-endian `ledouble`) - **String**: Byte sequences with length limits - **Bounds checking**: Prevents buffer overruns @@ -124,6 +127,26 @@ pub fn read_typed_value( The `read_byte` function signature changed in v0.2.0 to accept three parameters (`buffer`, `offset`, and `signed`) instead of two, allowing explicit control over signed vs unsigned byte interpretation. +**Floating-Point Type Reading (`evaluator/types/float.rs`):** + +```rust +pub fn read_float( + buffer: &[u8], + offset: usize, + endian: Endianness, +) -> Result + +pub fn read_double( + buffer: &[u8], + offset: usize, + endian: Endianness, +) -> Result +``` + +- `read_float()` reads 4 bytes and interprets as `f32`, converting to `f64` and returning `Value::Float(f64)` +- `read_double()` reads 8 bytes and interprets as `f64`, returning `Value::Float(f64)` +- Both respect endianness specified in `TypeKind::Float` or `TypeKind::Double` + ### Operator Application (`evaluator/operators.rs`) Applies comparison operations: @@ -139,6 +162,24 @@ Applies comparison operations: Comparison operators support numeric comparisons across different integer types using `i128` coercion for cross-type compatibility. +**Floating-Point Operator Semantics:** + +Float values (`Value::Float`) work with comparison and equality operators but have special handling: + +- **Equality operators** (`==`, `!=`): Use epsilon-aware comparison with `f64::EPSILON` tolerance + - Two floats are considered equal when `|a - b| <= f64::EPSILON` + - Implementation is in `floats_equal()` helper function (`evaluator/operators/equality.rs`) +- **Ordering operators** (`<`, `>`, `<=`, `>=`): Use IEEE 754 `partial_cmp` semantics + - Standard floating-point ordering: `-∞ < finite values < +∞` + - Implementation is in `compare_values()` function (`evaluator/operators/comparison.rs`) +- **NaN handling**: + - `NaN != NaN` returns `true` (NaN is never equal to anything, including itself) + - All comparison operations with NaN return `false` (NaN is not comparable) +- **Infinity handling**: + - Positive and negative infinity are only equal to the same sign of infinity + - Infinities are ordered correctly: `NEG_INFINITY < finite < INFINITY` +- **Type mismatch**: Float values cannot be compared with `Int` or `Uint` (returns `false` or `None`) + ```rust pub fn apply_operator( operator: &Operator, @@ -175,6 +216,41 @@ assert!(apply_operator( )); ``` +**Example with floating-point operators:** + +```rust +use libmagic_rs::parser::ast::{Operator, Value}; +use libmagic_rs::evaluator::operators::apply_operator; + +// Epsilon-aware equality +assert!(apply_operator( + &Operator::Equal, + &Value::Float(1.0), + &Value::Float(1.0 + f64::EPSILON) +)); + +// Float ordering +assert!(apply_operator( + &Operator::LessThan, + &Value::Float(1.5), + &Value::Float(2.0) +)); + +// NaN inequality +assert!(apply_operator( + &Operator::NotEqual, + &Value::Float(f64::NAN), + &Value::Float(f64::NAN) +)); + +// Infinity comparison +assert!(apply_operator( + &Operator::LessThan, + &Value::Float(f64::NEG_INFINITY), + &Value::Float(0.0) +)); +``` + ## Evaluation Algorithm The evaluator uses a depth-first hierarchical algorithm: @@ -362,17 +438,37 @@ let matches = evaluate_rules(&rules, &buffer)?; assert_eq!(matches[0].message, "Small value detected"); ``` +**Example with floating-point types:** + +```rust +use libmagic_rs::{evaluate_rules, EvaluationConfig}; +use libmagic_rs::parser::parse_text_magic_file; + +// Parse magic rule with float type +let magic_content = r#" +0 lefloat 3.14159 Pi constant detected +0 bedouble >100.0 Large double value +"#; +let rules = parse_text_magic_file(magic_content)?; + +// IEEE 754 little-endian representation of 3.14159f32 +let buffer = vec![0xd0, 0x0f, 0x49, 0x40]; +let matches = evaluate_rules(&rules, &buffer)?; + +assert_eq!(matches[0].message, "Pi constant detected"); +``` + ## Implementation Status - [x] Basic evaluation engine structure - [x] Offset resolution (absolute, relative, from-end) -- [x] Type reading with endianness support (Byte, Short, Long, Quad, String) +- [x] Type reading with endianness support (Byte, Short, Long, Quad, Float, Double, String) - [x] Operator application (Equal, NotEqual, LessThan, GreaterThan, LessEqual, GreaterEqual, BitwiseAnd, BitwiseAndMask) - [x] Hierarchical rule processing with child evaluation - [x] Error handling with graceful degradation - [x] Timeout protection - [x] Recursion depth limiting -- [x] Comprehensive test coverage (100+ tests) +- [x] Comprehensive test coverage (150+ tests) - [ ] Indirect offset support (pointer dereferencing) - [ ] Regex type support - [ ] Performance optimizations (rule ordering, caching) diff --git a/docs/src/magic-format.md b/docs/src/magic-format.md index 8d830940..6c907754 100644 --- a/docs/src/magic-format.md +++ b/docs/src/magic-format.md @@ -150,6 +150,33 @@ Examples: 8 uquad >0x8000000000000000 (unsigned 64-bit check) ``` +### Floating-Point Types + +| Type | Size | Endianness | IEEE 754 | +| ---------- | ------- | ------------- | -------- | +| `float` | 4 bytes | native | 32-bit | +| `befloat` | 4 bytes | big-endian | 32-bit | +| `lefloat` | 4 bytes | little-endian | 32-bit | +| `double` | 8 bytes | native | 64-bit | +| `bedouble` | 8 bytes | big-endian | 64-bit | +| `ledouble` | 8 bytes | little-endian | 64-bit | + +Floating-point types follow IEEE 754 standard. Unlike integer types, float types do not have signed or unsigned variants (the IEEE 754 format handles sign internally). + +Examples: + +```text +0 lefloat =3.14159 File with float value pi +0 bedouble >1.0 Double value greater than 1.0 +``` + +Float comparison behavior: + +- **Equality**: Uses epsilon-aware comparison (`f64::EPSILON` tolerance) +- **Ordering**: Uses IEEE 754 semantics via `partial_cmp` +- **NaN**: `NaN != NaN`, comparisons with NaN always return false +- **Infinity**: Positive and negative infinity are properly ordered + ### String Type Match literal string data: @@ -384,6 +411,19 @@ Output: `GIF image data, version 89a` >24 byte 6 \b, RGBA ``` +### Floating-Point Values + +```text +# Check for specific float value +0 lefloat =3.14159 File with float value pi + +# Float comparison +0 float >1.0 Float value greater than 1.0 + +# Double precision +0 bedouble =0.45455 PNG image with gamma 0.45455 +``` + ## Best Practices ### 1. Order Rules by Specificity @@ -461,6 +501,7 @@ Consider: - Relative offsets - Indirect offsets (basic) - Byte, short, long, quad types (8-bit, 16-bit, 32-bit, 64-bit integers) +- Float and double types (32-bit and 64-bit IEEE 754 floating-point) - String type - Comparison operators (equal, not-equal, less-than, greater-than, less-equal, greater-equal) - Bitwise AND operator @@ -471,7 +512,6 @@ Consider: - Regex patterns - Date/time types -- Float types - 128-bit integer types - Use/name directives - Default rules @@ -480,6 +520,7 @@ Consider: - **Strength modifiers**: The `!:strength` directive for adjusting rule priority - **64-bit integers**: `quad` type family (`quad`, `uquad`, `lequad`, `ulequad`, `bequad`, `ubequad`) +- **Floating-point types**: `float` and `double` type families (`float`, `befloat`, `lefloat`, `double`, `bedouble`, `ledouble`) with IEEE 754 semantics and epsilon-aware equality ## Troubleshooting diff --git a/docs/src/parser.md b/docs/src/parser.md index 6923f90c..2d599141 100644 --- a/docs/src/parser.md +++ b/docs/src/parser.md @@ -104,6 +104,11 @@ Handles multiple value types with intelligent type detection: parse_value("\"Hello\"") // Value::String("Hello".to_string()) parse_value("\"Line1\\nLine2\"") // Value::String("Line1\nLine2".to_string()) +// Floating-point literals +parse_value("3.14") // Value::Float(3.14) +parse_value("-1.0") // Value::Float(-1.0) +parse_value("2.5e10") // Value::Float(2.5e10) + // Numeric values parse_value("123") // Value::Uint(123) parse_value("-456") // Value::Int(-456) @@ -117,11 +122,68 @@ parse_value("7f454c46") // Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]) **Features:** - ✅ Quoted string parsing with escape sequence support +- ✅ Floating-point literal parsing with scientific notation support - ✅ Numeric literal parsing (decimal and hexadecimal) - ✅ Hex byte sequence parsing (with and without `\x` prefix) - ✅ Intelligent type precedence to avoid parsing conflicts - ✅ Comprehensive escape sequence handling (`\n`, `\t`, `\r`, `\\`, `\"`, `\'`, `\0`) +### Float and Double Type Parsing (`parse_float_value`) + +Parses floating-point type specifiers and literals for IEEE 754 single (32-bit) and double-precision (64-bit) values: + +```rust +// Float literals +parse_float_value("3.14") // Ok(("", 3.14)) +parse_float_value("-0.5") // Ok(("", -0.5)) +parse_float_value("1.0e-10") // Ok(("", 1.0e-10)) +parse_float_value("2.5E+3") // Ok(("", 2.5e+3)) +``` + +**Type Keywords:** + +Six floating-point type keywords are supported, each mapping to `TypeKind::Float` or `TypeKind::Double` with an `Endianness` field: + +- `float` - 32-bit IEEE 754, native endianness → `TypeKind::Float { endian: Endianness::Native }` +- `befloat` - 32-bit IEEE 754, big-endian → `TypeKind::Float { endian: Endianness::Big }` +- `lefloat` - 32-bit IEEE 754, little-endian → `TypeKind::Float { endian: Endianness::Little }` +- `double` - 64-bit IEEE 754, native endianness → `TypeKind::Double { endian: Endianness::Native }` +- `bedouble` - 64-bit IEEE 754, big-endian → `TypeKind::Double { endian: Endianness::Big }` +- `ledouble` - 64-bit IEEE 754, little-endian → `TypeKind::Double { endian: Endianness::Little }` + +**Float Literal Grammar:** + +The `parse_float_value` function recognizes standard floating-point notation with a **mandatory decimal point** to distinguish floats from integers: + +```text +[-]digits.digits[{e|E}[{+|-}]digits] +``` + +Examples: `3.14`, `-0.5`, `1.0e-10`, `2.5E+3` + +Parsed literals are stored as `Value::Float(f64)` in the AST, regardless of whether the rule uses `float` or `double` (the type determines buffer read size, not literal representation). + +**Usage in Magic Rules:** + +```rust +// Native-endian float comparison +0 float x // Match any float value +0 float =3.14 // Match if float equals 3.14 + +// Big-endian double comparison +0 bedouble >1.5 // Match if big-endian double > 1.5 +``` + +**Features:** + +- ✅ Six type keywords for float and double with endianness variants +- ✅ Float literal parsing with decimal point, negative values, scientific notation +- ✅ `Value::Float(f64)` AST variant for floating-point literals +- ✅ Type precedence ensures floats parsed before integers (decimal point disambiguates) +- ✅ Comprehensive test coverage for all endianness variants and literal formats + +**Note:** Float and double types do **not** have signed/unsigned variants. IEEE 754 handles sign internally via the sign bit, so all float types use a single `TypeKind` variant with only an `endian` field (no `signed: bool` field). + ## Parser Design Principles ### Error Handling @@ -211,7 +273,7 @@ assert_eq!(rules.len(), 1); // One root rule assert_eq!(rules[0].children.len(), 2); // Two child rules ``` -The parser distinguishes between signed and unsigned type variants (e.g., `byte` vs `ubyte`, `leshort` vs `uleshort`), mapping them to the `signed` field in `TypeKind::Byte { signed: bool }` and similar type variants. Unprefixed types default to signed in accordance with libmagic conventions. +The parser distinguishes between signed and unsigned type variants (e.g., `byte` vs `ubyte`, `leshort` vs `uleshort`), mapping them to the `signed` field in `TypeKind::Byte { signed: bool }` and similar type variants. Unprefixed types default to signed in accordance with libmagic conventions. Float and double types do not have signed/unsigned variants; IEEE 754 handles sign internally. ### Format Detection diff --git a/src/build_helpers.rs b/src/build_helpers.rs index 75a028b1..cde5fd3e 100644 --- a/src/build_helpers.rs +++ b/src/build_helpers.rs @@ -222,6 +222,85 @@ mod tests { assert!(serialized2.contains("signed: false")); } + #[test] + fn test_serialize_type_kind_float() { + let cases = [ + ( + TypeKind::Float { + endian: Endianness::Native, + }, + "TypeKind::Float { endian: Endianness::Native }", + ), + ( + TypeKind::Float { + endian: Endianness::Little, + }, + "TypeKind::Float { endian: Endianness::Little }", + ), + ( + TypeKind::Float { + endian: Endianness::Big, + }, + "TypeKind::Float { endian: Endianness::Big }", + ), + ]; + for (typ, expected) in &cases { + assert_eq!(serialize_type_kind(typ), *expected); + } + } + + #[test] + fn test_serialize_type_kind_double() { + let cases = [ + ( + TypeKind::Double { + endian: Endianness::Native, + }, + "TypeKind::Double { endian: Endianness::Native }", + ), + ( + TypeKind::Double { + endian: Endianness::Little, + }, + "TypeKind::Double { endian: Endianness::Little }", + ), + ( + TypeKind::Double { + endian: Endianness::Big, + }, + "TypeKind::Double { endian: Endianness::Big }", + ), + ]; + for (typ, expected) in &cases { + assert_eq!(serialize_type_kind(typ), *expected); + } + } + + #[test] + fn test_serialize_value_float() { + // Positive finite literal + let serialized = serialize_value(&Value::Float(3.125)); + assert_eq!(serialized, "Value::Float(3.125)"); + + // Negative finite literal + let serialized = serialize_value(&Value::Float(-1.0)); + assert_eq!(serialized, "Value::Float(-1.0)"); + + // Non-finite values produce valid Rust expressions + assert_eq!( + serialize_value(&Value::Float(f64::NAN)), + "Value::Float(f64::NAN)" + ); + assert_eq!( + serialize_value(&Value::Float(f64::INFINITY)), + "Value::Float(f64::INFINITY)" + ); + assert_eq!( + serialize_value(&Value::Float(f64::NEG_INFINITY)), + "Value::Float(f64::NEG_INFINITY)" + ); + } + #[test] fn test_serialize_type_kind_string() { let typ1 = TypeKind::String { max_length: None }; diff --git a/src/evaluator/engine/mod.rs b/src/evaluator/engine/mod.rs index 29692fb2..76cc9e4e 100644 --- a/src/evaluator/engine/mod.rs +++ b/src/evaluator/engine/mod.rs @@ -212,6 +212,7 @@ pub fn evaluate_rules( offset: absolute_offset, level: rule.level, value: read_value, + type_kind: rule.typ.clone(), confidence: RuleMatch::calculate_confidence(rule.level), }; matches.push(match_result); diff --git a/src/evaluator/mod.rs b/src/evaluator/mod.rs index 77b591d8..7f91c49b 100644 --- a/src/evaluator/mod.rs +++ b/src/evaluator/mod.rs @@ -213,6 +213,11 @@ pub struct RuleMatch { pub level: u32, /// The matched value pub value: crate::parser::ast::Value, + /// The type used to read the matched value + /// + /// Carries the source `TypeKind` so downstream consumers (e.g., output + /// formatting) can determine the on-disk width of the matched value. + pub type_kind: crate::parser::ast::TypeKind, /// Confidence score (0.0 to 1.0) /// /// Calculated based on match depth in the rule hierarchy. diff --git a/src/evaluator/operators/comparison.rs b/src/evaluator/operators/comparison.rs index 7d4a4be5..71dc1b75 100644 --- a/src/evaluator/operators/comparison.rs +++ b/src/evaluator/operators/comparison.rs @@ -32,6 +32,7 @@ pub fn compare_values(left: &Value, right: &Value) -> Option { (Value::Int(a), Value::Int(b)) => Some(a.cmp(b)), (Value::Uint(a), Value::Int(b)) => Some(i128::from(*a).cmp(&i128::from(*b))), (Value::Int(a), Value::Uint(b)) => Some(i128::from(*a).cmp(&i128::from(*b))), + (Value::Float(a), Value::Float(b)) => a.partial_cmp(b), (Value::String(a), Value::String(b)) => Some(a.cmp(b)), (Value::Bytes(a), Value::Bytes(b)) => Some(a.cmp(b)), _ => None, @@ -189,6 +190,91 @@ mod tests { assert_eq!(compare_values(&Value::Int(1), &Value::Bytes(vec![1])), None); } + #[test] + fn test_compare_values_float_ordering() { + use std::cmp::Ordering::*; + + assert_eq!( + compare_values(&Value::Float(1.0), &Value::Float(2.0)), + Some(Less) + ); + assert_eq!( + compare_values(&Value::Float(2.0), &Value::Float(2.0)), + Some(Equal) + ); + assert_eq!( + compare_values(&Value::Float(3.0), &Value::Float(2.0)), + Some(Greater) + ); + assert_eq!( + compare_values(&Value::Float(-1.0), &Value::Float(1.0)), + Some(Less) + ); + + // Infinity ordering + assert_eq!( + compare_values(&Value::Float(1.0), &Value::Float(f64::INFINITY)), + Some(Less) + ); + assert_eq!( + compare_values(&Value::Float(f64::NEG_INFINITY), &Value::Float(1.0)), + Some(Less) + ); + assert_eq!( + compare_values(&Value::Float(f64::INFINITY), &Value::Float(f64::INFINITY)), + Some(Equal) + ); + + // NaN is not comparable + assert_eq!( + compare_values(&Value::Float(f64::NAN), &Value::Float(1.0)), + None + ); + assert_eq!( + compare_values(&Value::Float(1.0), &Value::Float(f64::NAN)), + None + ); + assert_eq!( + compare_values(&Value::Float(f64::NAN), &Value::Float(f64::NAN)), + None + ); + + // Float vs non-float is incomparable + assert_eq!(compare_values(&Value::Float(1.0), &Value::Uint(1)), None); + assert_eq!(compare_values(&Value::Int(1), &Value::Float(1.0)), None); + } + + #[test] + fn test_comparison_operators_float() { + // Direct partial_cmp semantics for ordering operators + assert!(apply_less_than(&Value::Float(1.0), &Value::Float(2.0))); + assert!(!apply_less_than(&Value::Float(2.0), &Value::Float(2.0))); + assert!(apply_greater_than(&Value::Float(3.0), &Value::Float(2.0))); + assert!(!apply_greater_than(&Value::Float(2.0), &Value::Float(2.0))); + assert!(apply_less_equal(&Value::Float(2.0), &Value::Float(2.0))); + assert!(apply_less_equal(&Value::Float(1.0), &Value::Float(2.0))); + assert!(apply_greater_equal(&Value::Float(2.0), &Value::Float(2.0))); + assert!(apply_greater_equal(&Value::Float(3.0), &Value::Float(2.0))); + + // NaN comparisons all return false + assert!(!apply_less_than( + &Value::Float(f64::NAN), + &Value::Float(1.0) + )); + assert!(!apply_greater_than( + &Value::Float(f64::NAN), + &Value::Float(1.0) + )); + assert!(!apply_less_equal( + &Value::Float(f64::NAN), + &Value::Float(1.0) + )); + assert!(!apply_greater_equal( + &Value::Float(f64::NAN), + &Value::Float(1.0) + )); + } + #[test] fn test_comparison_operators_consistency() { // Verify all four comparison functions agree with compare_values diff --git a/src/evaluator/operators/equality.rs b/src/evaluator/operators/equality.rs index 4edfcb7b..aba3f4c8 100644 --- a/src/evaluator/operators/equality.rs +++ b/src/evaluator/operators/equality.rs @@ -9,11 +9,39 @@ use crate::parser::ast::Value; use super::compare_values; +/// Machine-epsilon threshold used when comparing `Value::Float` operands for +/// equality. Two floats are considered equal when `|a - b| <= FLOAT_EPSILON`. +/// Special values (NaN, infinity) are handled explicitly before the epsilon +/// check. +const FLOAT_EPSILON: f64 = f64::EPSILON; + +/// Return `true` when two `f64` values are considered equal under +/// epsilon-aware semantics. +/// +/// * **NaN**: NaN is never equal to anything (including itself). +/// * **Infinity**: positive/negative infinity are only equal to the same sign +/// of infinity. +/// * **Finite**: `|a - b| <= FLOAT_EPSILON`. +fn floats_equal(a: f64, b: f64) -> bool { + if a.is_nan() || b.is_nan() { + return false; + } + // Infinities: equal only when same sign (inf - inf = NaN, so must check first). + // Exact comparison is correct here -- infinities have precise IEEE 754 bit patterns. + if a.is_infinite() || b.is_infinite() { + #[allow(clippy::float_cmp)] + return a == b; + } + (a - b).abs() <= FLOAT_EPSILON +} + /// Apply equality comparison between two values /// /// Compares two `Value` instances for equality, handling proper type matching. /// Cross-type integer comparisons (`Uint` vs `Int`) are supported via `i128` -/// coercion. Incompatible types (e.g., string vs integer) are considered unequal. +/// coercion. Float comparisons use epsilon-aware equality +/// (`|a - b| <= f64::EPSILON`). Incompatible types (e.g., string vs integer) +/// are considered unequal. /// /// # Arguments /// @@ -22,7 +50,8 @@ use super::compare_values; /// /// # Returns /// -/// `true` if the values are equal (including cross-type integer coercion), `false` otherwise +/// `true` if the values are equal (including cross-type integer coercion and +/// epsilon-aware float comparison), `false` otherwise /// /// # Examples /// @@ -44,9 +73,15 @@ use super::compare_values; /// &Value::String("hello".to_string()), /// &Value::String("hello".to_string()) /// )); +/// +/// // Float epsilon-aware equality +/// assert!(apply_equal(&Value::Float(1.0), &Value::Float(1.0))); /// ``` #[must_use] pub fn apply_equal(left: &Value, right: &Value) -> bool { + if let (Value::Float(a), Value::Float(b)) = (left, right) { + return floats_equal(*a, *b); + } compare_values(left, right) == Some(Ordering::Equal) } @@ -726,6 +761,96 @@ mod tests { assert!(apply_not_equal(&empty_bytes, &empty_string)); } + // ============================================================ + // Float epsilon-aware equality / inequality tests + // ============================================================ + + #[test] + fn test_apply_equal_float_exact_same_value() { + assert!(apply_equal(&Value::Float(1.0), &Value::Float(1.0))); + assert!(apply_equal(&Value::Float(0.0), &Value::Float(0.0))); + assert!(apply_equal(&Value::Float(-3.125), &Value::Float(-3.125))); + } + + #[test] + fn test_apply_equal_float_near_equal_within_epsilon() { + // Values that differ by exactly f64::EPSILON should be considered equal + let a = 1.0_f64; + let b = a + f64::EPSILON; + assert!( + apply_equal(&Value::Float(a), &Value::Float(b)), + "values differing by f64::EPSILON should be equal" + ); + } + + #[test] + fn test_apply_equal_float_clearly_unequal() { + assert!(!apply_equal(&Value::Float(1.0), &Value::Float(2.0))); + assert!(!apply_equal(&Value::Float(0.0), &Value::Float(1.0))); + assert!(!apply_equal(&Value::Float(-1.0), &Value::Float(1.0))); + } + + #[test] + fn test_apply_equal_float_infinity() { + let pos_inf = f64::INFINITY; + let neg_inf = f64::NEG_INFINITY; + + assert!(apply_equal(&Value::Float(pos_inf), &Value::Float(pos_inf))); + assert!(apply_equal(&Value::Float(neg_inf), &Value::Float(neg_inf))); + assert!(!apply_equal(&Value::Float(pos_inf), &Value::Float(neg_inf))); + assert!(!apply_equal(&Value::Float(pos_inf), &Value::Float(1.0))); + } + + #[test] + fn test_apply_equal_float_nan() { + let nan = f64::NAN; + assert!(!apply_equal(&Value::Float(nan), &Value::Float(nan))); + assert!(!apply_equal(&Value::Float(nan), &Value::Float(0.0))); + assert!(!apply_equal(&Value::Float(0.0), &Value::Float(nan))); + } + + #[test] + fn test_apply_not_equal_float_exact_same_value() { + assert!(!apply_not_equal(&Value::Float(1.0), &Value::Float(1.0))); + assert!(!apply_not_equal(&Value::Float(0.0), &Value::Float(0.0))); + } + + #[test] + fn test_apply_not_equal_float_near_equal_within_epsilon() { + let a = 1.0_f64; + let b = a + f64::EPSILON; + assert!( + !apply_not_equal(&Value::Float(a), &Value::Float(b)), + "values differing by f64::EPSILON should not be not-equal" + ); + } + + #[test] + fn test_apply_not_equal_float_clearly_unequal() { + assert!(apply_not_equal(&Value::Float(1.0), &Value::Float(2.0))); + assert!(apply_not_equal(&Value::Float(-1.0), &Value::Float(1.0))); + } + + #[test] + fn test_apply_not_equal_float_nan() { + let nan = f64::NAN; + // NaN != NaN should be true (NaN is never equal to anything) + assert!(apply_not_equal(&Value::Float(nan), &Value::Float(nan))); + assert!(apply_not_equal(&Value::Float(nan), &Value::Float(0.0))); + } + + #[test] + fn test_apply_not_equal_float_infinity() { + assert!(!apply_not_equal( + &Value::Float(f64::INFINITY), + &Value::Float(f64::INFINITY) + )); + assert!(apply_not_equal( + &Value::Float(f64::INFINITY), + &Value::Float(f64::NEG_INFINITY) + )); + } + #[test] fn test_apply_not_equal_various_value_combinations() { // Test various combinations to ensure comprehensive coverage diff --git a/src/evaluator/strength.rs b/src/evaluator/strength.rs index 1342144b..4ee12593 100644 --- a/src/evaluator/strength.rs +++ b/src/evaluator/strength.rs @@ -77,10 +77,10 @@ pub fn calculate_default_strength(rule: &MagicRule) -> i32 { // Add bonus for limited-length strings (more constrained match) if max_length.is_some() { base + 5 } else { base } } - // 64-bit integers are most specific among numerics - TypeKind::Quad { .. } => 16, - // 32-bit integers are fairly specific - TypeKind::Long { .. } => 15, + // 64-bit types are most specific among numerics + TypeKind::Quad { .. } | TypeKind::Double { .. } => 16, + // 32-bit types are fairly specific + TypeKind::Long { .. } | TypeKind::Float { .. } => 15, // 16-bit integers are moderately specific TypeKind::Short { .. } => 10, // Single bytes are least specific @@ -132,7 +132,7 @@ pub fn calculate_default_strength(rule: &MagicRule) -> i32 { i32::try_from(b.len()).unwrap_or(20).min(20) } // Numeric values don't get length bonus - Value::Uint(_) | Value::Int(_) => 0, + Value::Uint(_) | Value::Int(_) | Value::Float(_) => 0, }; strength += value_length_bonus; diff --git a/src/evaluator/tests.rs b/src/evaluator/tests.rs index 97e918fc..c73cc658 100644 --- a/src/evaluator/tests.rs +++ b/src/evaluator/tests.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use super::*; -use crate::parser::ast::Value; +use crate::parser::ast::{TypeKind, Value}; #[test] fn test_evaluation_context_new() { @@ -338,6 +338,7 @@ fn test_rule_match_creation() { offset: 0, level: 0, value: Value::Uint(0x7f), + type_kind: TypeKind::Byte { signed: false }, confidence: RuleMatch::calculate_confidence(0), }; @@ -355,6 +356,7 @@ fn test_rule_match_clone() { offset: 42, level: 1, value: Value::String("test".to_string()), + type_kind: TypeKind::String { max_length: None }, confidence: RuleMatch::calculate_confidence(1), }; @@ -369,6 +371,7 @@ fn test_rule_match_debug() { offset: 10, level: 2, value: Value::Bytes(vec![0x01, 0x02]), + type_kind: TypeKind::Byte { signed: false }, confidence: RuleMatch::calculate_confidence(2), }; diff --git a/src/evaluator/types/float.rs b/src/evaluator/types/float.rs new file mode 100644 index 00000000..50cc7072 --- /dev/null +++ b/src/evaluator/types/float.rs @@ -0,0 +1,340 @@ +// Copyright (c) 2025-2026 the libmagic-rs contributors +// SPDX-License-Identifier: Apache-2.0 + +use super::TypeReadError; +use crate::parser::ast::{Endianness, Value}; +use byteorder::{BigEndian, ByteOrder, LittleEndian, NativeEndian}; + +/// Safely reads a 32-bit IEEE 754 float from the buffer at the specified offset. +/// +/// The result is widened to `f64` and returned as `Value::Float(f64)`. +/// +/// # Arguments +/// +/// * `buffer` - The byte buffer to read from +/// * `offset` - The offset position to start reading from +/// * `endian` - The byte order to use when interpreting the bytes +/// +/// # Examples +/// +/// ``` +/// use libmagic_rs::evaluator::types::read_float; +/// use libmagic_rs::parser::ast::{Endianness, Value}; +/// +/// // IEEE 754 little-endian representation of 1.0f32: 0x3f800000 +/// let buffer = &[0x00, 0x00, 0x80, 0x3f]; +/// let result = read_float(buffer, 0, Endianness::Little).unwrap(); +/// assert_eq!(result, Value::Float(1.0)); +/// ``` +/// +/// # Errors +/// +/// Returns `TypeReadError::BufferOverrun` if fewer than 4 bytes are available at the +/// requested offset. +pub fn read_float( + buffer: &[u8], + offset: usize, + endian: Endianness, +) -> Result { + let end = offset.checked_add(4).ok_or(TypeReadError::BufferOverrun { + offset, + buffer_len: buffer.len(), + })?; + let bytes = buffer + .get(offset..end) + .ok_or(TypeReadError::BufferOverrun { + offset, + buffer_len: buffer.len(), + })?; + + let value = match endian { + Endianness::Little => LittleEndian::read_f32(bytes), + Endianness::Big => BigEndian::read_f32(bytes), + Endianness::Native => NativeEndian::read_f32(bytes), + }; + + Ok(Value::Float(f64::from(value))) +} + +/// Safely reads a 64-bit IEEE 754 double from the buffer at the specified offset. +/// +/// # Arguments +/// +/// * `buffer` - The byte buffer to read from +/// * `offset` - The offset position to start reading from +/// * `endian` - The byte order to use when interpreting the bytes +/// +/// # Examples +/// +/// ``` +/// use libmagic_rs::evaluator::types::read_double; +/// use libmagic_rs::parser::ast::{Endianness, Value}; +/// +/// // IEEE 754 big-endian representation of 1.0f64: 0x3ff0000000000000 +/// let buffer = &[0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; +/// let result = read_double(buffer, 0, Endianness::Big).unwrap(); +/// assert_eq!(result, Value::Float(1.0)); +/// ``` +/// +/// # Errors +/// +/// Returns `TypeReadError::BufferOverrun` if fewer than 8 bytes are available at the +/// requested offset. +pub fn read_double( + buffer: &[u8], + offset: usize, + endian: Endianness, +) -> Result { + let end = offset.checked_add(8).ok_or(TypeReadError::BufferOverrun { + offset, + buffer_len: buffer.len(), + })?; + let bytes = buffer + .get(offset..end) + .ok_or(TypeReadError::BufferOverrun { + offset, + buffer_len: buffer.len(), + })?; + + let value = match endian { + Endianness::Little => LittleEndian::read_f64(bytes), + Endianness::Big => BigEndian::read_f64(bytes), + Endianness::Native => NativeEndian::read_f64(bytes), + }; + + Ok(Value::Float(value)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_read_float_endianness() { + // IEEE 754 representation of 1.0f32: 0x3f800000 + let cases: Vec<(&[u8], Endianness, f64)> = vec![ + // Little-endian: bytes reversed + (&[0x00, 0x00, 0x80, 0x3f], Endianness::Little, 1.0), + // Big-endian: bytes in order + (&[0x3f, 0x80, 0x00, 0x00], Endianness::Big, 1.0), + // -2.5f32 = 0xc0200000 + ( + &[0x00, 0x00, 0x20, 0xc0], + Endianness::Little, + f64::from(-2.5_f32), + ), + ( + &[0xc0, 0x20, 0x00, 0x00], + Endianness::Big, + f64::from(-2.5_f32), + ), + // 0.0f32 = 0x00000000 + (&[0x00, 0x00, 0x00, 0x00], Endianness::Little, 0.0), + (&[0x00, 0x00, 0x00, 0x00], Endianness::Big, 0.0), + ]; + + for (buffer, endian, expected) in cases { + let result = read_float(buffer, 0, endian).unwrap(); + assert_eq!( + result, + Value::Float(expected), + "endian={endian:?}, expected={expected}" + ); + } + } + + #[test] + fn test_read_float_native_endian() { + // 1.0f32 bytes in both possible orders + let le_bytes = &[0x00, 0x00, 0x80, 0x3f]; + let be_bytes = &[0x3f, 0x80, 0x00, 0x00]; + + let le_result = read_float(le_bytes, 0, Endianness::Native); + let be_result = read_float(be_bytes, 0, Endianness::Native); + + // One of these should produce 1.0 + #[allow(clippy::float_cmp)] + let either_is_one = matches!(le_result, Ok(Value::Float(v)) if v == 1.0) + || matches!(be_result, Ok(Value::Float(v)) if v == 1.0); + assert!(either_is_one, "Native endian should match one byte order"); + } + + #[test] + fn test_read_float_at_offset() { + // Two bytes of padding, then 1.0f32 in big-endian + let buffer = &[0xaa, 0xbb, 0x3f, 0x80, 0x00, 0x00]; + let result = read_float(buffer, 2, Endianness::Big).unwrap(); + assert_eq!(result, Value::Float(1.0)); + } + + #[test] + fn test_read_float_returns_value_float() { + let buffer = &[0x00, 0x00, 0x80, 0x3f]; // 1.0f32 LE + match read_float(buffer, 0, Endianness::Little).unwrap() { + Value::Float(_) => {} + other => panic!("Expected Value::Float, got {other:?}"), + } + } + + #[test] + fn test_read_float_buffer_overrun() { + // Too few bytes + assert_eq!( + read_float(&[0x00, 0x00, 0x80], 0, Endianness::Little).unwrap_err(), + TypeReadError::BufferOverrun { + offset: 0, + buffer_len: 3, + } + ); + + // Empty buffer + assert_eq!( + read_float(&[], 0, Endianness::Big).unwrap_err(), + TypeReadError::BufferOverrun { + offset: 0, + buffer_len: 0, + } + ); + + // Offset past end + assert_eq!( + read_float(&[0x00; 8], 6, Endianness::Little).unwrap_err(), + TypeReadError::BufferOverrun { + offset: 6, + buffer_len: 8, + } + ); + } + + #[test] + fn test_read_float_offset_overflow() { + let buffer = &[0x00; 4]; + assert_eq!( + read_float(buffer, usize::MAX, Endianness::Little).unwrap_err(), + TypeReadError::BufferOverrun { + offset: usize::MAX, + buffer_len: 4, + } + ); + } + + #[test] + fn test_read_double_endianness() { + // IEEE 754 representation of 1.0f64: 0x3ff0000000000000 + let cases: Vec<(&[u8], Endianness, f64)> = vec![ + // Little-endian + ( + &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f], + Endianness::Little, + 1.0, + ), + // Big-endian + ( + &[0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + Endianness::Big, + 1.0, + ), + // -2.5f64 = 0xc004000000000000 + ( + &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xc0], + Endianness::Little, + -2.5, + ), + ( + &[0xc0, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + Endianness::Big, + -2.5, + ), + // 0.0f64 = all zeros + ( + &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + Endianness::Little, + 0.0, + ), + ]; + + for (buffer, endian, expected) in cases { + let result = read_double(buffer, 0, endian).unwrap(); + assert_eq!( + result, + Value::Float(expected), + "endian={endian:?}, expected={expected}" + ); + } + } + + #[test] + fn test_read_double_native_endian() { + let le_bytes = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f]; + let be_bytes = &[0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + + let le_result = read_double(le_bytes, 0, Endianness::Native); + let be_result = read_double(be_bytes, 0, Endianness::Native); + + #[allow(clippy::float_cmp)] + let either_is_one = matches!(le_result, Ok(Value::Float(v)) if v == 1.0) + || matches!(be_result, Ok(Value::Float(v)) if v == 1.0); + assert!(either_is_one, "Native endian should match one byte order"); + } + + #[test] + fn test_read_double_at_offset() { + // Three bytes of padding, then 1.0f64 in big-endian + let buffer = &[ + 0xaa, 0xbb, 0xcc, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + let result = read_double(buffer, 3, Endianness::Big).unwrap(); + assert_eq!(result, Value::Float(1.0)); + } + + #[test] + fn test_read_double_returns_value_float() { + let buffer = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f]; // 1.0f64 LE + match read_double(buffer, 0, Endianness::Little).unwrap() { + Value::Float(_) => {} + other => panic!("Expected Value::Float, got {other:?}"), + } + } + + #[test] + fn test_read_double_buffer_overrun() { + // Too few bytes + assert_eq!( + read_double(&[0x00; 7], 0, Endianness::Little).unwrap_err(), + TypeReadError::BufferOverrun { + offset: 0, + buffer_len: 7, + } + ); + + // Empty buffer + assert_eq!( + read_double(&[], 0, Endianness::Big).unwrap_err(), + TypeReadError::BufferOverrun { + offset: 0, + buffer_len: 0, + } + ); + + // Offset past end + assert_eq!( + read_double(&[0x00; 16], 10, Endianness::Little).unwrap_err(), + TypeReadError::BufferOverrun { + offset: 10, + buffer_len: 16, + } + ); + } + + #[test] + fn test_read_double_offset_overflow() { + let buffer = &[0x00; 8]; + assert_eq!( + read_double(buffer, usize::MAX, Endianness::Little).unwrap_err(), + TypeReadError::BufferOverrun { + offset: usize::MAX, + buffer_len: 8, + } + ); + } +} diff --git a/src/evaluator/types/mod.rs b/src/evaluator/types/mod.rs index 0224d28e..f26d0c49 100644 --- a/src/evaluator/types/mod.rs +++ b/src/evaluator/types/mod.rs @@ -6,12 +6,14 @@ //! This module exposes the public type-reading API and dispatches to focused //! submodules for numeric and string handling. +mod float; mod numeric; mod string; use crate::parser::ast::{TypeKind, Value}; use thiserror::Error; +pub use float::{read_double, read_float}; pub use numeric::{read_byte, read_long, read_quad, read_short}; pub use string::read_string; @@ -71,6 +73,8 @@ pub fn read_typed_value( TypeKind::Short { endian, signed } => read_short(buffer, offset, *endian, *signed), TypeKind::Long { endian, signed } => read_long(buffer, offset, *endian, *signed), TypeKind::Quad { endian, signed } => read_quad(buffer, offset, *endian, *signed), + TypeKind::Float { endian } => read_float(buffer, offset, *endian), + TypeKind::Double { endian } => read_double(buffer, offset, *endian), TypeKind::String { max_length } => read_string(buffer, offset, *max_length), } } @@ -109,6 +113,10 @@ pub fn coerce_value_to_type(value: &Value, type_kind: &TypeKind) -> Value { #[allow(clippy::cast_possible_wrap)] Value::Int(*v as i64) } + // Round f64 expected value to f32 precision for TypeKind::Float so that + // parsed f64 literals compare correctly against f32-widened file values. + #[allow(clippy::cast_possible_truncation)] + (Value::Float(v), TypeKind::Float { .. }) => Value::Float(f64::from(*v as f32)), _ => value.clone(), } } diff --git a/src/evaluator/types/tests.rs b/src/evaluator/types/tests.rs index eea0a90e..9cb7c0e0 100644 --- a/src/evaluator/types/tests.rs +++ b/src/evaluator/types/tests.rs @@ -92,6 +92,65 @@ fn test_read_typed_value_numeric_dispatch() { assert_eq!(quad, Value::Uint(0x1234_5678_90ab_cdef)); } +#[test] +fn test_read_typed_value_float_dispatch() { + // IEEE 754 little-endian 1.0f32: 0x3f800000 + let float_result = read_typed_value( + &[0x00, 0x00, 0x80, 0x3f], + 0, + &TypeKind::Float { + endian: Endianness::Little, + }, + ) + .unwrap(); + assert_eq!(float_result, Value::Float(1.0)); + + // IEEE 754 big-endian 1.0f64: 0x3ff0000000000000 + let double_result = read_typed_value( + &[0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + 0, + &TypeKind::Double { + endian: Endianness::Big, + }, + ) + .unwrap(); + assert_eq!(double_result, Value::Float(1.0)); + + // Float buffer overrun: 3 bytes is too few for a 4-byte float + let float_err = read_typed_value( + &[0x00, 0x00, 0x80], + 0, + &TypeKind::Float { + endian: Endianness::Little, + }, + ) + .unwrap_err(); + assert_eq!( + float_err, + TypeReadError::BufferOverrun { + offset: 0, + buffer_len: 3, + } + ); + + // Double buffer overrun: 7 bytes is too few for an 8-byte double + let double_err = read_typed_value( + &[0x00; 7], + 0, + &TypeKind::Double { + endian: Endianness::Big, + }, + ) + .unwrap_err(); + assert_eq!( + double_err, + TypeReadError::BufferOverrun { + offset: 0, + buffer_len: 7, + } + ); +} + #[test] fn test_read_typed_value_native_endian() { let result = read_typed_value( @@ -218,6 +277,35 @@ fn test_read_typed_value_all_supported_types() { } } +#[test] +fn test_coerce_value_to_type_float_rounds_to_f32() { + // 0.1 as f64 differs from 0.1 as f32-widened-to-f64 + let f64_val = Value::Float(0.1_f64); + let coerced = coerce_value_to_type( + &f64_val, + &TypeKind::Float { + endian: Endianness::Native, + }, + ); + // After coercion, value should match f32 precision + #[allow(clippy::cast_possible_truncation)] + let expected = f64::from(0.1_f64 as f32); + assert_eq!(coerced, Value::Float(expected)); +} + +#[test] +fn test_coerce_value_to_type_double_preserves_f64() { + // Double should not alter the f64 value + let val = Value::Float(0.1_f64); + let coerced = coerce_value_to_type( + &val, + &TypeKind::Double { + endian: Endianness::Native, + }, + ); + assert_eq!(coerced, Value::Float(0.1_f64)); +} + #[test] fn test_read_typed_value_signed_vs_unsigned() { let buffer = &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; @@ -451,6 +539,21 @@ fn test_coerce_value_to_type() { TypeKind::String { max_length: None }, Value::Uint(0xff), ), + ( + Value::Float(3.125), + TypeKind::Float { + endian: Endianness::Native, + }, + // 3.125 rounded to f32 precision then widened back to f64 + Value::Float(f64::from(3.125_f32)), + ), + ( + Value::Float(3.125), + TypeKind::Double { + endian: Endianness::Native, + }, + Value::Float(3.125), + ), ]; for (i, (input, type_kind, expected)) in cases.iter().enumerate() { diff --git a/src/output/json.rs b/src/output/json.rs index 1976137f..9e72167b 100644 --- a/src/output/json.rs +++ b/src/output/json.rs @@ -289,6 +289,15 @@ pub fn format_value_as_hex(value: &Value) -> String { } result } + Value::Float(f) => { + // Convert to little-endian bytes for consistency + let bytes = f.to_le_bytes(); + let mut result = String::with_capacity(16); // 8 bytes * 2 chars per byte + for &b in &bytes { + write!(&mut result, "{b:02x}").expect("Writing to String should never fail"); + } + result + } } } diff --git a/src/output/mod.rs b/src/output/mod.rs index d61ad9b7..c0889982 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -195,6 +195,7 @@ impl MatchResult { Value::Bytes(bytes) => bytes.len(), Value::String(s) => s.len(), Value::Uint(_) | Value::Int(_) => std::mem::size_of::(), + Value::Float(_) => std::mem::size_of::(), }, value, rule_path: Vec::new(), @@ -274,13 +275,13 @@ impl MatchResult { #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let confidence = (m.confidence * 100.0).min(100.0) as u8; - // TODO: Numeric length is hardcoded to 4 bytes. Value::Uint/Int don't encode - // their source width, so byte/short/long/quad all report 4. Carrying TypeKind - // in RuleMatch would allow accurate lengths (1, 2, 4, 8). let length = match &m.value { Value::Bytes(b) => b.len(), Value::String(s) => s.len(), - Value::Uint(_) | Value::Int(_) => 4, + Value::Uint(_) | Value::Int(_) | Value::Float(_) => m + .type_kind + .bit_width() + .map_or(0, |bits| (bits / 8) as usize), }; Self::with_metadata( diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 2540e9ff..b4ae0a7b 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -115,6 +115,34 @@ pub enum TypeKind { /// Whether value is signed signed: bool, }, + /// 32-bit IEEE 754 floating-point + /// + /// # Examples + /// + /// ``` + /// use libmagic_rs::parser::ast::{TypeKind, Endianness}; + /// + /// let float = TypeKind::Float { endian: Endianness::Big }; + /// assert_eq!(float, TypeKind::Float { endian: Endianness::Big }); + /// ``` + Float { + /// Byte order + endian: Endianness, + }, + /// 64-bit IEEE 754 double-precision floating-point + /// + /// # Examples + /// + /// ``` + /// use libmagic_rs::parser::ast::{TypeKind, Endianness}; + /// + /// let double = TypeKind::Double { endian: Endianness::Big }; + /// assert_eq!(double, TypeKind::Double { endian: Endianness::Big }); + /// ``` + Double { + /// Byte order + endian: Endianness, + }, /// String data String { /// Maximum length to read @@ -134,6 +162,8 @@ impl TypeKind { /// assert_eq!(TypeKind::Short { endian: Endianness::Native, signed: true }.bit_width(), Some(16)); /// assert_eq!(TypeKind::Long { endian: Endianness::Native, signed: true }.bit_width(), Some(32)); /// assert_eq!(TypeKind::Quad { endian: Endianness::Native, signed: true }.bit_width(), Some(64)); + /// assert_eq!(TypeKind::Float { endian: Endianness::Native }.bit_width(), Some(32)); + /// assert_eq!(TypeKind::Double { endian: Endianness::Native }.bit_width(), Some(64)); /// assert_eq!(TypeKind::String { max_length: None }.bit_width(), None); /// ``` #[must_use] @@ -141,8 +171,8 @@ impl TypeKind { match self { Self::Byte { .. } => Some(8), Self::Short { .. } => Some(16), - Self::Long { .. } => Some(32), - Self::Quad { .. } => Some(64), + Self::Long { .. } | Self::Float { .. } => Some(32), + Self::Quad { .. } | Self::Double { .. } => Some(64), Self::String { .. } => None, } } @@ -275,12 +305,23 @@ pub enum Operator { } /// Value types for rule matching -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Value { /// Unsigned integer value Uint(u64), /// Signed integer value Int(i64), + /// Floating-point value (used for `float` and `double` types) + /// + /// # Examples + /// + /// ``` + /// use libmagic_rs::parser::ast::Value; + /// + /// let val = Value::Float(3.14); + /// assert_eq!(val, Value::Float(3.14)); + /// ``` + Float(f64), /// Byte sequence Bytes(Vec), /// String value @@ -585,14 +626,19 @@ mod tests { // Test that different value types are not equal let uint_val = Value::Uint(42); let int_val = Value::Int(42); + let float_val = Value::Float(42.0); let bytes_val = Value::Bytes(vec![42]); let string_val = Value::String("42".to_string()); assert_ne!(uint_val, int_val); + assert_ne!(uint_val, float_val); assert_ne!(uint_val, bytes_val); assert_ne!(uint_val, string_val); + assert_ne!(int_val, float_val); assert_ne!(int_val, bytes_val); assert_ne!(int_val, string_val); + assert_ne!(float_val, bytes_val); + assert_ne!(float_val, string_val); assert_ne!(bytes_val, string_val); } @@ -625,11 +671,24 @@ mod tests { } } + #[test] + fn test_value_float() { + let value = Value::Float(3.125); + assert_eq!(value, Value::Float(3.125)); + + let negative = Value::Float(-1.5); + assert_eq!(negative, Value::Float(-1.5)); + + let zero = Value::Float(0.0); + assert_eq!(zero, Value::Float(0.0)); + } + #[test] fn test_value_serialization() { let values = vec![ Value::Uint(42), Value::Int(-100), + Value::Float(3.125), Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]), Value::String("ELF executable".to_string()), ]; @@ -741,6 +800,18 @@ mod tests { endian: Endianness::Big, signed: true, }, + TypeKind::Float { + endian: Endianness::Native, + }, + TypeKind::Float { + endian: Endianness::Big, + }, + TypeKind::Double { + endian: Endianness::Little, + }, + TypeKind::Double { + endian: Endianness::Native, + }, TypeKind::String { max_length: None }, TypeKind::String { max_length: Some(128), diff --git a/src/parser/codegen.rs b/src/parser/codegen.rs index f17708b4..38474e3b 100644 --- a/src/parser/codegen.rs +++ b/src/parser/codegen.rs @@ -187,6 +187,14 @@ pub fn serialize_type_kind(typ: &TypeKind) -> String { serialize_endianness(*endian), signed ), + TypeKind::Float { endian } => format!( + "TypeKind::Float {{ endian: {} }}", + serialize_endianness(*endian) + ), + TypeKind::Double { endian } => format!( + "TypeKind::Double {{ endian: {} }}", + serialize_endianness(*endian) + ), TypeKind::String { max_length } => match max_length { Some(value) => { format!("TypeKind::String {{ max_length: Some({value}) }}") @@ -218,6 +226,17 @@ pub fn serialize_value(value: &Value) -> String { match value { Value::Uint(number) => format!("Value::Uint({})", format_number(*number)), Value::Int(number) => format!("Value::Int({})", format_signed_number(*number)), + Value::Float(f) => { + if f.is_nan() { + "Value::Float(f64::NAN)".to_string() + } else if *f == f64::INFINITY { + "Value::Float(f64::INFINITY)".to_string() + } else if *f == f64::NEG_INFINITY { + "Value::Float(f64::NEG_INFINITY)".to_string() + } else { + format!("Value::Float({f:?})") + } + } Value::Bytes(bytes) => format!("Value::Bytes({})", format_byte_vec(bytes)), Value::String(text) => format!( "Value::String(String::from({}))", diff --git a/src/parser/grammar/mod.rs b/src/parser/grammar/mod.rs index 6909cd0e..fa587fa0 100644 --- a/src/parser/grammar/mod.rs +++ b/src/parser/grammar/mod.rs @@ -538,6 +538,39 @@ fn parse_quoted_string(input: &str) -> IResult<&str, String> { Ok((remaining, result)) } +/// Parse a floating-point literal into `Value::Float(f64)` +/// +/// Recognizes numbers with a mandatory decimal point (to distinguish from +/// integers), an optional leading minus sign, and an optional exponent part. +/// Examples: `3.14`, `-1.0`, `2.5e10`, `-0.5E-3` +fn parse_float_value(input: &str) -> IResult<&str, f64> { + let (input, _) = multispace0(input)?; + + let (remaining, float_str) = recognize(( + opt(char('-')), + digit1, + char('.'), + digit1, + opt((one_of("eE"), opt(one_of("+-")), digit1)), + )) + .parse(input)?; + + let value: f64 = float_str + .parse() + .map_err(|_| nom::Err::Error(NomError::new(input, nom::error::ErrorKind::MapRes)))?; + + // Reject non-finite floats (NaN, +inf, -inf) to keep AST, JSON, and codegen valid + if !value.is_finite() { + return Err(nom::Err::Error(NomError::new( + input, + nom::error::ErrorKind::Float, + ))); + } + + let (remaining, _) = multispace0(remaining)?; + Ok((remaining, value)) +} + /// Parse a numeric value (integer) /// /// Non-negative literals are parsed directly as `u64` so the full unsigned @@ -560,10 +593,11 @@ fn parse_numeric_value(input: &str) -> IResult<&str, Value> { Ok((input, value)) } -/// Parse string and numeric literals for magic rule values +/// Parse string, float, and numeric literals for magic rule values /// /// Supports: /// - Quoted strings with escape sequences: "Hello\nWorld", "ELF\0" +/// - Floating-point literals: 3.14, -1.0, 2.5e10 /// - Numeric literals (decimal): 123, -456 /// - Numeric literals (hexadecimal): 0x1a2b, -0xFF /// - Hex byte sequences: \\x7f\\x45\\x4c\\x46 or 7f454c46 @@ -613,6 +647,8 @@ pub fn parse_value(input: &str) -> IResult<&str, Value> { map(parse_quoted_string, Value::String), // Try hex byte sequence before numeric (to catch patterns like "7f", "ab", "\\x7fELF", etc.) map(parse_hex_bytes, Value::Bytes), + // Try float before integer (a float literal is a superset of an integer prefix) + map(parse_float_value, Value::Float), // Try numeric value last (for pure numbers like 0x123, 1, etc.) parse_numeric_value, )) diff --git a/src/parser/grammar/tests.rs b/src/parser/grammar/tests.rs index b088f3ef..6d0293de 100644 --- a/src/parser/grammar/tests.rs +++ b/src/parser/grammar/tests.rs @@ -905,6 +905,9 @@ fn test_parse_value_with_whitespace() { ); assert_eq!(parse_value(" 123 "), Ok(("", Value::Uint(123)))); assert_eq!(parse_value("\t-456\t"), Ok(("", Value::Int(-456)))); + // Floats consume trailing whitespace (consistent with integers) + assert_eq!(parse_value(" 3.125 "), Ok(("", Value::Float(3.125)))); + assert_eq!(parse_value("\t-1.0\t"), Ok(("", Value::Float(-1.0)))); // Hex bytes don't consume trailing whitespace by themselves assert_eq!( parse_value(" \\x7f\\x45 "), @@ -1012,6 +1015,40 @@ fn test_parse_value_type_precedence() { assert_eq!(parse_value("0x123"), Ok(("", Value::Uint(0x123)))); } +#[test] +fn test_parse_value_float_literals() { + // Positive floats + assert_eq!(parse_value("3.125"), Ok(("", Value::Float(3.125)))); + assert_eq!(parse_value("0.5"), Ok(("", Value::Float(0.5)))); + assert_eq!(parse_value("100.0"), Ok(("", Value::Float(100.0)))); + + // Negative floats + assert_eq!(parse_value("-1.0"), Ok(("", Value::Float(-1.0)))); + assert_eq!(parse_value("-0.001"), Ok(("", Value::Float(-0.001)))); + + // Scientific notation + assert_eq!(parse_value("2.5e10"), Ok(("", Value::Float(2.5e10)))); + assert_eq!(parse_value("1.0E-3"), Ok(("", Value::Float(1.0e-3)))); + assert_eq!(parse_value("-3.0e+2"), Ok(("", Value::Float(-3.0e+2)))); + + // Integers should NOT be parsed as floats + assert_eq!(parse_value("123"), Ok(("", Value::Uint(123)))); + assert_eq!(parse_value("-456"), Ok(("", Value::Int(-456)))); + assert_eq!(parse_value("0x1a"), Ok(("", Value::Uint(26)))); + + // Float with remaining input (trailing whitespace consumed, like integers) + assert_eq!(parse_value("1.5 rest"), Ok(("rest", Value::Float(1.5)))); + + // Non-finite floats (overflow to infinity) should never produce Value::Float + // parse_value falls through to other parsers, so we check the result type + if let Ok((_, value)) = parse_value("1.0e309") { + assert!( + !matches!(value, Value::Float(f) if !f.is_finite()), + "overflow should not produce non-finite Value::Float" + ); + } +} + #[test] fn test_parse_value_boundary_conditions() { // Test boundary conditions for different value types @@ -1188,7 +1225,6 @@ fn test_parse_type_invalid() { assert!(parse_type("").is_err()); assert!(parse_type("invalid").is_err()); assert!(parse_type("int").is_err()); - assert!(parse_type("float").is_err()); } #[test] diff --git a/src/parser/types.rs b/src/parser/types.rs index 012b1530..6b19202a 100644 --- a/src/parser/types.rs +++ b/src/parser/types.rs @@ -71,6 +71,15 @@ pub fn parse_type_keyword(input: &str) -> IResult<&str, &str> { )), // 8-bit types (2 branches) alt((tag("ubyte"), tag("byte"))), + // Float/double types (6 branches) + alt(( + tag("bedouble"), + tag("ledouble"), + tag("double"), + tag("befloat"), + tag("lefloat"), + tag("float"), + )), // String types (1 branch, will grow with pstring/search/regex) tag("string"), )) @@ -193,6 +202,28 @@ pub fn type_keyword_to_kind(type_name: &str) -> TypeKind { signed: false, }, + // FLOAT types (32-bit) + "float" => TypeKind::Float { + endian: Endianness::Native, + }, + "befloat" => TypeKind::Float { + endian: Endianness::Big, + }, + "lefloat" => TypeKind::Float { + endian: Endianness::Little, + }, + + // DOUBLE types (64-bit) + "double" => TypeKind::Double { + endian: Endianness::Native, + }, + "bedouble" => TypeKind::Double { + endian: Endianness::Big, + }, + "ledouble" => TypeKind::Double { + endian: Endianness::Little, + }, + // STRING type "string" => TypeKind::String { max_length: None }, @@ -370,7 +401,8 @@ mod tests { let keywords = [ "byte", "ubyte", "short", "ushort", "leshort", "uleshort", "beshort", "ubeshort", "long", "ulong", "lelong", "ulelong", "belong", "ubelong", "quad", "uquad", "lequad", - "ulequad", "bequad", "ubequad", "string", + "ulequad", "bequad", "ubequad", "float", "befloat", "lefloat", "double", "bedouble", + "ledouble", "string", ]; for keyword in keywords { let (rest, parsed) = parse_type_keyword(keyword).unwrap(); diff --git a/src/tests.rs b/src/tests.rs index 81fcfc6a..467dc605 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -379,6 +379,7 @@ fn test_concatenate_messages_simple() { offset: 0, level: 0, value: Value::Bytes(vec![0x7f]), + type_kind: TypeKind::Byte { signed: false }, confidence: 0.3, }, evaluator::RuleMatch { @@ -386,6 +387,7 @@ fn test_concatenate_messages_simple() { offset: 4, level: 1, value: Value::Uint(2), + type_kind: TypeKind::Byte { signed: false }, confidence: 0.5, }, ]; @@ -402,6 +404,7 @@ fn test_concatenate_messages_with_backspace() { offset: 0, level: 0, value: Value::Bytes(vec![0x7f]), + type_kind: TypeKind::Byte { signed: false }, confidence: 0.3, }, evaluator::RuleMatch { @@ -409,6 +412,7 @@ fn test_concatenate_messages_with_backspace() { offset: 4, level: 1, value: Value::Uint(2), + type_kind: TypeKind::Byte { signed: false }, confidence: 0.5, }, ]; diff --git a/tests/evaluator_tests.rs b/tests/evaluator_tests.rs index 3e2e2fbb..6a7cd89c 100644 --- a/tests/evaluator_tests.rs +++ b/tests/evaluator_tests.rs @@ -3,10 +3,15 @@ //! Evaluator integration tests //! -//! Tests for confidence calculation, rule ordering, and evaluation behavior -//! through the public `MagicDatabase` API. +//! Tests for confidence calculation, rule ordering, and evaluation behavior. +//! Uses both the public `MagicDatabase` API and the lower-level `evaluate_rules` +//! function for type-specific evaluation scenarios. -use libmagic_rs::{EvaluationConfig, MagicDatabase}; +use libmagic_rs::evaluator::evaluate_rules; +use libmagic_rs::{ + Endianness, EvaluationConfig, EvaluationContext, MagicDatabase, MagicRule, OffsetSpec, + Operator, TypeKind, Value, +}; // ============================================================ // Confidence Calculation Tests @@ -245,3 +250,133 @@ fn test_evaluate_partial_magic_header() { // Should not crash, might not match assert!(!result.description.is_empty()); } + +// ============================================================ +// Float / Double Evaluation Tests +// ============================================================ + +#[test] +fn test_evaluate_float_rule_equal() { + // IEEE 754 little-endian 1.0f32 = 0x3f800000 => bytes [0x00, 0x00, 0x80, 0x3f] + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Float { + endian: Endianness::Little, + }, + op: Operator::Equal, + value: Value::Float(1.0), + message: "float 1.0 detected".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer: &[u8] = &[0x00, 0x00, 0x80, 0x3f]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + let matches = evaluate_rules(&[rule], buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1, "Float equal rule should match 1.0f32 LE"); +} + +#[test] +fn test_evaluate_double_rule_equal() { + // IEEE 754 big-endian 1.0f64 = 0x3ff0000000000000 + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Double { + endian: Endianness::Big, + }, + op: Operator::Equal, + value: Value::Float(1.0), + message: "double 1.0 detected".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer: &[u8] = &[0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + let matches = evaluate_rules(&[rule], buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1, "Double equal rule should match 1.0f64 BE"); +} + +#[test] +fn test_evaluate_float_rule_not_equal() { + // Buffer contains 1.0f32 LE, rule expects != 2.0 -- should match + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Float { + endian: Endianness::Little, + }, + op: Operator::NotEqual, + value: Value::Float(2.0), + message: "not 2.0".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer: &[u8] = &[0x00, 0x00, 0x80, 0x3f]; // 1.0f32 LE + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + let matches = evaluate_rules(&[rule], buffer, &mut context).unwrap(); + assert_eq!( + matches.len(), + 1, + "Float not-equal rule should match when value differs" + ); +} + +#[test] +fn test_evaluate_float_rule_less_than() { + // Buffer contains 1.0f32 LE, rule checks < 2.0 -- should match + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Float { + endian: Endianness::Little, + }, + op: Operator::LessThan, + value: Value::Float(2.0), + message: "less than 2.0".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer: &[u8] = &[0x00, 0x00, 0x80, 0x3f]; // 1.0f32 LE + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + let matches = evaluate_rules(&[rule], buffer, &mut context).unwrap(); + assert_eq!( + matches.len(), + 1, + "Float less-than rule should match 1.0 < 2.0" + ); +} + +#[test] +fn test_evaluate_float_rule_no_match() { + // Buffer contains 1.0f32 LE, rule expects == 2.0 -- should NOT match + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Float { + endian: Endianness::Little, + }, + op: Operator::Equal, + value: Value::Float(2.0), + message: "should not match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer: &[u8] = &[0x00, 0x00, 0x80, 0x3f]; // 1.0f32 LE + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + let matches = evaluate_rules(&[rule], buffer, &mut context).unwrap(); + assert!( + matches.is_empty(), + "Float equal rule should not match when value differs" + ); +} diff --git a/tests/property_tests.rs b/tests/property_tests.rs index 230afc40..4b3ef852 100644 --- a/tests/property_tests.rs +++ b/tests/property_tests.rs @@ -43,6 +43,8 @@ fn arb_type_kind() -> impl Strategy { .prop_map(|(endian, signed)| { TypeKind::Long { endian, signed } }), (arb_endianness(), any::()) .prop_map(|(endian, signed)| { TypeKind::Quad { endian, signed } }), + arb_endianness().prop_map(|endian| TypeKind::Float { endian }), + arb_endianness().prop_map(|endian| TypeKind::Double { endian }), (0usize..256usize).prop_map(|len| TypeKind::String { max_length: Some(len), }), @@ -71,6 +73,7 @@ fn arb_value() -> impl Strategy { prop_oneof![ (0u64..=u32::MAX as u64).prop_map(Value::Uint), (i32::MIN as i64..=i32::MAX as i64).prop_map(Value::Int), + (-1e10f64..1e10f64).prop_map(Value::Float), prop::collection::vec(any::(), 0..32).prop_map(Value::Bytes), "[a-zA-Z0-9 ]{0,32}".prop_map(Value::String), ]