shashank created CAMEL-25013:
--------------------------------

             Summary: Simple and PredicateBuilder comparisons truncate decimals 
when the two numbers have different types: ${header.amount} > 100 is false for 
a BigDecimal 100.50, and 2.5 == 2 is true
                 Key: CAMEL-25013
                 URL: https://issues.apache.org/jira/browse/CAMEL-25013
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank


{{ObjectHelper.typeCoerceCompare}} 
({{core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java:281-363}})
 has fast paths for two values of the same type and for String/number pairs. 
When both values are numbers of *different* types ({{Double}} vs {{Integer}}, 
{{BigDecimal}} vs {{Integer}}, {{BigDecimal}} vs {{Double}}, {{Float}} vs 
{{Long}}, ...), none of them applies and the code falls through to:
{code:java}
// if both values is numeric then compare using numeric
Long leftNum = converter.tryConvertTo(Long.class, leftValue);
Long rightNum = converter.tryConvertTo(Long.class, rightValue);
if (leftNum != null && rightNum != null) {
    return leftNum.compareTo(rightNum);
}
{code}
The type converter converts a {{Number}} to {{Long}} with {{longValue()}} 
({{ObjectConverter.toLong(Number)}}), which drops the fractional part. So 
{{2.5}} and {{2}} compare as equal.

{{typeCoerceEquals}} has the same problem in {{tryConverters}} 
({{ObjectHelper.java:142-170}}): it converts the left value to the class of the 
right value, so {{Double 2.5}} becomes {{Integer 2}}, and {{2.5 == 2}} is true.

These methods back all Simple comparison operators ({{>}}, {{>=}}, {{<}}, 
{{<=}}, {{==}}, {{!=}}, {{range}}) and the Java DSL predicates 
({{header("x").isGreaterThan(...)}}, {{isEqualTo(...)}}). The left side is 
often a {{Double}} (JSONPath, Jackson) or a {{BigDecimal}} (JDBC/SQL). The 
right side is often an integer literal. The result is silent wrong routing: no 
error and no log.

*Reproduction* (standalone program against main):
{noformat}
typeCoerceCompare(Double 2.5, Integer 2)          = 0    (expected > 0)
typeCoerceCompare(BigDecimal 100.50, Integer 100) = 0    (expected > 0)
typeCoerceCompare(BigDecimal 2.5, Double 2.0)     = 0    (expected > 0)
typeCoerceCompare(Double -0.9, Integer 0)         = 0    (expected < 0)
typeCoerceEquals(Double 2.5, Integer 2)           = true (expected false)
typeCoerceEquals(BigDecimal 99.99, Long 99)       = true (expected false)

header p = 2.5 (Double, BigDecimal or Float):
  ${header.p} > 2            -> false
  ${header.p} == 2           -> true
  ${header.p} != 2           -> false
  ${header.p} <= 2           -> true
  ${header.p} range '1..2'   -> true
  ${header.p} > 2.0          -> false for BigDecimal and Float (true for Double)

from("direct:order").choice()
    .when(simple("${header.amount} > 100")).to("large")
    .otherwise().to("small");
  amount = new BigDecimal("100.50")  -> small
  amount = 100.75d                   -> small
from("direct:java").filter(header("amount").isGreaterThan(100)) ...
  amount = BigDecimal 100.50 or Double 100.75 -> filtered out
{noformat}
Controls behave correctly: same-type values, the String "2.5" vs "2" 
(CAMEL-21109), and a String header "100.50". The same happens for a String 
right-hand side against a {{BigDecimal}}: {{typeCoerceCompare(BigDecimal 2.5, 
"2") = 0}}. A property-based test (3000 random Double/BigDecimal vs Integer 
pairs) shrinks the failure to {{0.01}} vs {{0}} comparing as equal.

Two {{BigDecimal}} (or two {{BigInteger}}) values have no fast path either and 
go through the same {{Long}} fallback: {{typeCoerceCompare(BigDecimal 2.5, 
BigDecimal 2.4) = 0}}, and a {{BigInteger}} that does not fit in a long wraps. 
{{typeCoerceEquals}} of two {{BigDecimal}} values uses {{BigDecimal.equals}}, 
which also compares the scale, so {{2.50 == 2.5}} is false while {{<=}} and 
{{>=}} are both true.

The same fallback code is in camel-2.25.4, camel-3.0.0, camel-4.0.0 and 
camel-4.14.0, so all maintained versions are affected.

A Lean model proves the following. For every integer n ≥ 0 and every digit d = 
1..9, the current code says {{n.d == n}}. Values without a fractional part 
always compare correctly, so the fix only changes the result when a value has a 
fractional part.

*Proposed fix:* in {{typeCoerceCompare}}, after the existing fast paths, 
compare two numbers, or a number and a numeric String, by their values: as 
{{long}} when both are integral, otherwise as {{BigDecimal}} 
({{Double}}/{{Float}} through their decimal representation, NaN and infinity as 
{{double}}). In {{typeCoerceEquals}}, do the same for two numbers of different 
types and for two {{BigDecimal}} values. {{2.0 == 2}} and {{2 < 2.5}} keep 
working, and {{2.5 == 2}} becomes false. Add an upgrade guide note, as existing 
routes may get a different result.

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to