Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/Horde/String.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
36 changes: 34 additions & 2 deletions src/CharacterSets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 4 additions & 0 deletions src/HordeString.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
75 changes: 75 additions & 0 deletions test/CharacterSetsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*/

namespace Horde\Util\Test;

use Horde\Util\CharacterSets;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

/**
* Tests for the CharacterSets class.
*
* @author Ralf Lang <ralf.lang@ralf-lang.de>
* @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'));
}
}
43 changes: 43 additions & 0 deletions test/HordeStringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading