From 558a41e3c95338590b172719e6dd25215d4e8450 Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Tue, 25 Aug 2026 02:40:01 +0100 Subject: [PATCH 1/4] Fix String::hash for utf16 strings --- include/hxString.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/include/hxString.h b/include/hxString.h index a140167eb..5fd402561 100644 --- a/include/hxString.h +++ b/include/hxString.h @@ -228,8 +228,8 @@ class HXCPP_EXTERN_CLASS_ATTRIBUTES String #ifdef HXCPP_PARANOID unsigned int result = calcHash(); - unsigned int have = (((unsigned int *)__s)[-1] & HX_GC_CONST_ALLOC_BIT) ? - ((unsigned int *)__s)[-2] : *((unsigned int *)(__s+length+1) ); + unsigned int have = (((unsigned int *)__s)[-1] & HX_GC_CONST_ALLOC_BIT) ? ((unsigned int *)__s)[-2] : + isUTF16Encoded() ? *((unsigned int *)(__w+length+1)) : *((unsigned int *)(__s+length+1)); if ( have != result ) { @@ -247,6 +247,13 @@ class HXCPP_EXTERN_CLASS_ATTRIBUTES String return ((unsigned int *)__s)[-2]; #endif } + if (isUTF16Encoded()) { + #ifdef __EMSCRIPTEN__ + return *((emscripten_align1_int *)(__w+length+1)); + #else + return *((unsigned int *)(__w+length+1)); + #endif + } #ifdef __EMSCRIPTEN__ return *((emscripten_align1_int *)(__s+length+1) ); #else From 98865cc897e1b0b17ad99c58f6251caed92ec957 Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Tue, 25 Aug 2026 03:22:16 +0100 Subject: [PATCH 2/4] [tests] Verify unicode string hashing --- test/haxe/TestStringHash.hx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/haxe/TestStringHash.hx b/test/haxe/TestStringHash.hx index 2cdaa3635..793a6ab4f 100644 --- a/test/haxe/TestStringHash.hx +++ b/test/haxe/TestStringHash.hx @@ -80,4 +80,21 @@ class TestStringHash extends Test Assert.pass(); } + public function testUnicode() + { + for (literal in ["รก", "๐Ÿ‘"]) { + var fromBytes = haxe.io.Bytes.ofString(literal).toString(); + + Assert.equals(literal, fromBytes); + + var m = new Map(); + m[literal] = 1; + + Assert.isTrue(m.exists(literal)); + Assert.isTrue(m.exists(fromBytes)); + + Assert.equals(1, m[literal]); + Assert.equals(1, m[fromBytes]); + } + } } From c61a4cca8fbdf0997af668219836d04c95c55ab4 Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Tue, 25 Aug 2026 03:34:38 +0100 Subject: [PATCH 3/4] Fix String::calcHash with non-BMP characters Outside the BMP, characters take two utf16 code units. The old hashing was incorrect as it would only ever read a single unit at a time, and therefore calculate the wrong hash for a string containing a non-BMP character. The Char16Advance call handles this, ensuring that surrogate pairs are read as a single code point. --- src/String.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/String.cpp b/src/String.cpp index 4d84b77b7..dd0cbca22 100644 --- a/src/String.cpp +++ b/src/String.cpp @@ -932,9 +932,10 @@ unsigned int String::calcSubHash(int start, int inLen) const if (isUTF16Encoded()) { const char16_t *w = __w + start; - for(int i=0;i Date: Tue, 25 Aug 2026 03:35:36 +0100 Subject: [PATCH 4/4] [tests] Verify non-BMP string hashing --- test/haxe/TestStringHash.hx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/haxe/TestStringHash.hx b/test/haxe/TestStringHash.hx index 793a6ab4f..19a2077fc 100644 --- a/test/haxe/TestStringHash.hx +++ b/test/haxe/TestStringHash.hx @@ -83,17 +83,22 @@ class TestStringHash extends Test public function testUnicode() { for (literal in ["รก", "๐Ÿ‘"]) { + var fromJson:String = haxe.Json.parse('\"$literal\"'); var fromBytes = haxe.io.Bytes.ofString(literal).toString(); + Assert.equals(literal, fromJson); + Assert.equals(fromJson, fromBytes); Assert.equals(literal, fromBytes); var m = new Map(); m[literal] = 1; Assert.isTrue(m.exists(literal)); + Assert.isTrue(m.exists(fromJson)); Assert.isTrue(m.exists(fromBytes)); Assert.equals(1, m[literal]); + Assert.equals(1, m[fromJson]); Assert.equals(1, m[fromBytes]); } }