shashank created CAMEL-26290:
--------------------------------

             Summary: camel-util - CaseInsensitiveMap treats keys that differ 
in the case of a supplementary letter as different keys, FilterIterator.remove 
after hasNext removes the wrong element, TimeUtils.isPositive is true for 
negative durations with a nanos part
                 Key: CAMEL-26290
                 URL: https://issues.apache.org/jira/browse/CAMEL-26290
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank
            Assignee: shashank


Three small defects in camel-util, found by a formal check of the 
specifications of these classes. One PR fixes all three.

*1. CaseInsensitiveMap: keys that differ only in the case of a supplementary 
letter are different keys*

{{CaseInsensitiveMap}} (the message headers map since 4.21, CAMEL-23691) 
compares keys with {{String.equalsIgnoreCase}}, which folds whole code points 
(JDK 16+), but {{caseInsensitiveHash}} folds each UTF-16 char on its own 
({{Character.toLowerCase(Character.toUpperCase(c))}}). For a letter outside the 
BMP the two chars of the surrogate pair are folded separately, which changes 
nothing. So two keys that are equal ignoring case get different hashes: U+10400 
(DESERET CAPITAL LETTER LONG I) and U+10428 (DESERET SMALL LETTER LONG I) are 
{{equalsIgnoreCase}}, but {{get}} and {{containsKey}} with the other case miss, 
and {{put}} of both stores two keys. The earlier 
{{TreeMap(String.CASE_INSENSITIVE_ORDER)}} based map treated them as one key. 
Scripts with such case pairs include Deseret, Osage, Old Hungarian, Warang 
Citi, Medefaidrin and Adlam (260 case pairs of code points on JDK 21). Header 
names are ASCII in practice, hence Minor.

*Fix*: the hash folds a surrogate pair as one code point, with the same 
two-step fold. ASCII keys keep the fast path, and BMP keys hash as before, so 
nothing else changes (no allocation, same iteration order). For well-formed 
strings the hash is now consistent with {{equalsIgnoreCase}}: a test checks 
every code point against its case variants, and 2 million random keys from case 
pairs, the Turkish i forms, the Kelvin sign and supplementary letters agree 
with {{equalsIgnoreCase}}. Only for malformed keys with unpaired surrogates, 
where {{equalsIgnoreCase}} itself is not an equivalence (it lines up the 
surrogates of the two strings differently), can a lookup still differ, as on 
main.

*2. FilterIterator.remove after hasNext removes the wrong element*

{{FilterIterator.hasNext()}} looks ahead: it takes the next matching element 
from the underlying iterator. {{remove()}} delegated to the underlying 
iterator, so after a look-ahead it removed the looked-at element instead of the 
one {{next()}} returned. On an {{ArrayList}} [a, b, c]: {{next()}} returns a, 
{{hasNext()}}, {{remove()}} removes b (leaves [a, c]), and the next {{next()}} 
returns the removed b. {{hasNext(); remove()}} before any {{next()}} removed a 
instead of throwing {{IllegalStateException}} as the {{Iterator}} contract 
requires.

*Fix*: {{remove()}} throws {{IllegalStateException}} unless it directly follows 
{{next()}} (not before {{next()}}, not twice, not after a {{hasNext()}} that 
looked ahead); otherwise it removes the returned element as before. Camel's own 
code does not use {{FilterIterator}}.

*3. TimeUtils.isPositive is true for negative durations with a nanos part*

{{TimeUtils.isPositive(Duration)}} returns {{dur.getSeconds() > 0 || 
dur.getNano() != 0}}. A {{Duration}} stores normalized seconds and a nanos part 
that is never negative, so {{Duration.ofNanos(-1)}} is -1 seconds + 999999999 
nanos, and {{isPositive}} returns true (also for -999999999 ns, -1000000001 ns 
or -1500 ms).

*Fix*: {{!dur.isNegative() && !dur.isZero()}}. Camel's own code does not call 
{{TimeUtils.isPositive}}.

*Tests*: new {{CaseInsensitiveMapSupplementaryCaseTest}} and 
{{FilterIteratorRemoveTest}}, two new methods in {{TimeUtilsTest}}, all in 
camel-util. 5 of them fail on main: 
{{supplementaryLetterInTheOtherCaseIsTheSameKey}}, 
{{hashIsTheSameForEveryCodePointAndItsCaseVariants}} (all 260 supplementary 
case pairs), {{removeAfterHasNextFailsAndRemovesNothing}}, 
{{removeBeforeNextFails}} and 
{{isPositiveIsFalseForNegativeDurationsWithANanosPart}}. The others (ASCII, 
BMP, Turkish dotless i, Kelvin sign and lone surrogate keys; remove twice, 
remove after next, remove after skipped elements; zero, positive and whole 
negative durations) pass on main and with the fix; they are controls. 
camel-util: 307 tests, 0 failures.

*How it was found*: a Rocq formalisation of the specifications of these 
classes. For {{CaseInsensitiveMap}} the spec "a map keyed by 
{{String.equalsIgnoreCase}}" could not be proved: {{cih_consistent_bmp}} proves 
that equal-ignoring-case keys without surrogates have the same hash, and 
{{cih_inconsistent_supplementary}} proves the U+10400 / U+10428 counterexample. 
For the two others, the compiled classes were run in a JVM model in Rocq: 
{{filter_remove_wrong_element}} and {{filter_remove_before_next}} for 
{{FilterIterator}}, {{negative_nanosecond_is_reported_positive}} and 
{{duration_controls}} for {{TimeUtils}}. All were then reproduced on a HotSpot 
JVM.

*Affected*: item 1: main and camel-4.22.x (since 4.21). Items 2 and 3: main, 
camel-4.22.x, camel-4.18.x and camel-4.14.x (same code; read, not run).

_Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to