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)