Shuo Cheng created FLINK-40265:
----------------------------------
Summary: BinaryStringData.compareTo uses inconsistent UTF-16 and
UTF-8 ordering for SMP characters
Key: FLINK-40265
URL: https://issues.apache.org/jira/browse/FLINK-40265
Project: Flink
Issue Type: Bug
Components: Table SQL / Runtime
Reporter: Shuo Cheng
h3. Description
{{BinaryStringData.compareTo}} uses two different comparison paths:
* If both values have a non-null {{javaObject}}, it delegates to
{{String.compareTo}}, which compares UTF-16 code units.
* Otherwise, it materializes both values and compares their UTF-8 bytes as
unsigned bytes.
These orders are not equivalent for comparisons between an SMP character and
BMP characters in the range U+E000-U+FFFF. As a result, the comparison result
depends on whether a {{BinaryStringData}} is Java-backed or binary-backed and
can change after a serializer round trip.
For example, compare GRINNING FACE (😀, U+1F600) with FULLWIDTH EXCLAMATION MARK
(!, U+FF01):
* UTF-16: D83D DE00 < FF01, so 😀 < !
* UTF-8: F0 9F 98 80 > EF BC 81, so 😀 > !
h3. Reproduce
{code:ajava}
@Test
void compareToIsConsistentAfterSerialization() throws IOException {
BinaryStringData grinningFace = BinaryStringData.fromString("😀");
BinaryStringData fullWidthExclamationMark =
BinaryStringData.fromString("!");
DataOutputSerializer output = new DataOutputSerializer(16);
StringDataSerializer.INSTANCE.serialize(grinningFace, output);
BinaryStringData deserializedGrinningFace =
(BinaryStringData)
StringDataSerializer.INSTANCE.deserialize(
new
DataInputDeserializer(output.getCopyOfBuffer()));
assertThat(deserializedGrinningFace).isEqualTo(grinningFace);
assertThat(deserializedGrinningFace.compareTo(fullWidthExclamationMark)).isPositive();
assertThat(grinningFace.compareTo(fullWidthExclamationMark))
.isEqualTo(deserializedGrinningFace.compareTo(fullWidthExclamationMark));
}
{code}
h3. Actual behavior
After serialization:
* {{grinningFace.compareTo(fullWidthExclamationMark)}} is negative because both
values retain Java objects and use {{String.compareTo}}.
* {{deserializedGrinningFace.compareTo(fullWidthExclamationMark)}} is positive
because the deserialized value is binary-backed and comparison uses UTF-8 bytes.
The final assertion fails.
h3. Expected behavior
{{BinaryStringData.compareTo}} should produce the same ordering regardless of
the backing representation and before/after serialization. The class Javadoc
and {{SortUtil.putStringNormalizedKey}} already describe/use UTF-8 byte
ordering, so comparison should follow one canonical ordering.
h3. Impact
This violates the {{Comparable}} contract. The original and deserialized values
compare equal to each other, but have opposite ordering relative to the same
third value. It can cause representation-dependent results in generated string
comparisons, sorting, MIN/MAX, or other operations using {{compareTo}}.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)