From a4d2786be3d3ec7c6f7ba71d360ca554becd3a28 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Wed, 1 Apr 2026 20:59:13 +0200 Subject: [PATCH] fix: backend character set name to iconv/mbstring name equivalent in one more place --- lib/Horde/String.php | 4 ++ src/CharacterSets.php | 36 +++++++++++++++++- src/HordeString.php | 4 ++ test/CharacterSetsTest.php | 75 ++++++++++++++++++++++++++++++++++++++ test/HordeStringTest.php | 43 ++++++++++++++++++++++ 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 test/CharacterSetsTest.php diff --git a/lib/Horde/String.php b/lib/Horde/String.php index c778993..a2e74db 100644 --- a/lib/Horde/String.php +++ b/lib/Horde/String.php @@ -118,6 +118,10 @@ public static function convertCharset($input, $from, $to, $force = false) */ protected static function _convertCharset($input, $from, $to) { + /* Normalize charset identifiers to handle common aliases (e.g., utf8mb4 → utf-8). */ + $from = CharacterSets::normalize($from); + $to = CharacterSets::normalize($to); + /* Early return for same charset (should already be handled by caller). */ $fromLower = self::lower($from); $toLower = self::lower($to); diff --git a/src/CharacterSets.php b/src/CharacterSets.php index 9655e5e..2c3d362 100644 --- a/src/CharacterSets.php +++ b/src/CharacterSets.php @@ -19,15 +19,47 @@ */ class CharacterSets { - private static array $toMbString = [ + /** + * Normalization map for character set aliases to canonical names. + * + * This map handles common charset aliases across different systems: + * - MySQL utf8mb3/utf8mb4 variants → utf-8 + * - Simple utf8 (no dash) → utf-8 + */ + private static array $normalizeMap = [ 'utf8mb3' => 'utf-8', 'utf8mb4' => 'utf-8', 'utf8' => 'utf-8', ]; + /** + * Normalize a character set identifier to a canonical name. + * + * This should be called before passing charset names to any conversion + * function (iconv, mbstring, UConverter) to handle common aliases. + * + * @param string $identifier The charset identifier to normalize. + * + * @return string The normalized charset identifier. + */ + public static function normalize(string $identifier): string + { + $lower = strtolower($identifier); + return self::$normalizeMap[$lower] ?? $identifier; + } + + /** + * Convert charset identifier to mbstring-compatible name. + * + * This applies normalization and any mbstring-specific mappings. + * + * @param string $identifier The charset identifier. + * + * @return string The mbstring-compatible charset name. + */ public static function toMbstring(string $identifier): string { // TODO: Check against mb_list_encoding - return self::$toMbString[$identifier] ?? $identifier; + return self::normalize($identifier); } } diff --git a/src/HordeString.php b/src/HordeString.php index f53d57b..af227b8 100644 --- a/src/HordeString.php +++ b/src/HordeString.php @@ -128,6 +128,10 @@ public static function convertCharset($input, $from, $to, $force = false) */ protected static function _convertCharset($input, $from, $to) { + /* Normalize charset identifiers to handle common aliases (e.g., utf8mb4 → utf-8). */ + $from = CharacterSets::normalize($from); + $to = CharacterSets::normalize($to); + /* Early return for same charset (should already be handled by caller). */ $fromLower = self::lower($from); $toLower = self::lower($to); diff --git a/test/CharacterSetsTest.php b/test/CharacterSetsTest.php new file mode 100644 index 0000000..a6fa216 --- /dev/null +++ b/test/CharacterSetsTest.php @@ -0,0 +1,75 @@ + + * @category Horde + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @package Util + */ +#[CoversClass(CharacterSets::class)] +class CharacterSetsTest extends TestCase +{ + public function testNormalizeUtf8mb4(): void + { + $this->assertEquals('utf-8', CharacterSets::normalize('utf8mb4')); + } + + public function testNormalizeUtf8mb4CaseInsensitive(): void + { + $this->assertEquals('utf-8', CharacterSets::normalize('UTF8MB4')); + $this->assertEquals('utf-8', CharacterSets::normalize('Utf8Mb4')); + } + + public function testNormalizeUtf8mb3(): void + { + $this->assertEquals('utf-8', CharacterSets::normalize('utf8mb3')); + } + + public function testNormalizeUtf8NoDash(): void + { + $this->assertEquals('utf-8', CharacterSets::normalize('utf8')); + } + + public function testNormalizeUtf8WithDash(): void + { + // utf-8 is already canonical, should return unchanged + $this->assertEquals('utf-8', CharacterSets::normalize('utf-8')); + } + + public function testNormalizeUnknownCharset(): void + { + // Unknown charsets should pass through unchanged + $this->assertEquals('iso-8859-1', CharacterSets::normalize('iso-8859-1')); + $this->assertEquals('windows-1252', CharacterSets::normalize('windows-1252')); + } + + public function testToMbstring(): void + { + // toMbstring should normalize utf8mb4 → utf-8 + $this->assertEquals('utf-8', CharacterSets::toMbstring('utf8mb4')); + $this->assertEquals('utf-8', CharacterSets::toMbstring('UTF8MB4')); + } + + public function testToMbstringPreservesOtherCharsets(): void + { + // Non-normalized charsets should pass through + $this->assertEquals('iso-8859-1', CharacterSets::toMbstring('iso-8859-1')); + } +} diff --git a/test/HordeStringTest.php b/test/HordeStringTest.php index 5f8868b..f04a6f8 100644 --- a/test/HordeStringTest.php +++ b/test/HordeStringTest.php @@ -995,4 +995,47 @@ public function testConvertCharsetWithIntegerArrayKeysDoesNotThrowTypeError() $this->assertArrayHasKey(1, $result); $this->assertArrayHasKey(2, $result); } + + /** + * Test conversion from MySQL utf8mb4 charset. + * + * MySQL reports its connection charset as 'utf8mb4' (UTF-8 with full + * 4-byte character support). This should be normalized to 'utf-8' and + * recognized as the same charset, requiring no conversion. + */ + public function testConvertCharsetFromUtf8mb4() + { + $testString = 'Hello World 👋 emoji test'; + + // utf8mb4 → utf-8 should return unchanged (same charset after normalization) + $result = HordeString::convertCharset($testString, 'utf8mb4', 'utf-8'); + $this->assertEquals($testString, $result); + + // utf8mb4 → UTF-8 (case insensitive) should also work + $result = HordeString::convertCharset($testString, 'UTF8MB4', 'UTF-8'); + $this->assertEquals($testString, $result); + + // utf-8 → utf8mb4 should also return unchanged + $result = HordeString::convertCharset($testString, 'utf-8', 'utf8mb4'); + $this->assertEquals($testString, $result); + } + + /** + * Test conversion from utf8mb3 charset. + * + * MySQL's utf8mb3 is UTF-8 with 3-byte character limit (excludes 4-byte + * emoji and some rare characters). Should also normalize to 'utf-8'. + */ + public function testConvertCharsetFromUtf8mb3() + { + $testString = 'Hello World'; + + // utf8mb3 → utf-8 should return unchanged + $result = HordeString::convertCharset($testString, 'utf8mb3', 'utf-8'); + $this->assertEquals($testString, $result); + + // Case insensitive + $result = HordeString::convertCharset($testString, 'UTF8MB3', 'UTF-8'); + $this->assertEquals($testString, $result); + } }