350 lines
10 KiB
PHP
350 lines
10 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Uwil\ArcadiaGenesisPhpClient\Utils\StringEncoder;
|
||
|
||
describe('toUtf8()', function (): void {
|
||
|
||
it('returns an empty string when given an empty string', function (): void {
|
||
// Given
|
||
$input = '';
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'UTF-8');
|
||
|
||
// Then
|
||
expect($result)->toBe('');
|
||
});
|
||
|
||
it('passes through a valid UTF-8 string with multibyte characters', function (): void {
|
||
// Given
|
||
$input = 'Héllo café résumé';
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'UTF-8');
|
||
|
||
// Then
|
||
expect($result)->toBe('Héllo café résumé');
|
||
});
|
||
|
||
it('throws ValueError for an invalid UTF-8 string', function (): void {
|
||
// Given
|
||
$input = "Invalid \xC3\x28 sequence";
|
||
|
||
// When / Then
|
||
StringEncoder::toUtf8($input, 'UTF-8');
|
||
})->throws(\ValueError::class, 'not valid UTF-8');
|
||
|
||
it('strips the UTF-8 BOM by default', function (): void {
|
||
// Given
|
||
$input = "\xEF\xBB\xBFBonjour";
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'UTF-8');
|
||
|
||
// Then
|
||
expect($result)->toBe('Bonjour');
|
||
});
|
||
|
||
it('preserves the UTF-8 BOM when stripBom is false', function (): void {
|
||
// Given
|
||
$input = "\xEF\xBB\xBFBonjour";
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'UTF-8', stripBom: false);
|
||
|
||
// Then
|
||
expect($result)->toBe("\xEF\xBB\xBFBonjour");
|
||
});
|
||
|
||
it('converts Windows-1252 special characters to UTF-8', function (int $byte, string $expectedUtf8): void {
|
||
// Given
|
||
$input = chr($byte);
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'Windows-1252');
|
||
|
||
// Then
|
||
expect($result)->toBe($expectedUtf8);
|
||
})->with([
|
||
'euro sign (0x80)' => [0x80, '€'],
|
||
'single low-9 quote (0x82)' => [0x82, '‚'],
|
||
'latin small f (0x83)' => [0x83, 'ƒ'],
|
||
'double low-9 quote (0x84)' => [0x84, '„'],
|
||
'ellipsis (0x85)' => [0x85, '…'],
|
||
'dagger (0x86)' => [0x86, '†'],
|
||
'double dagger (0x87)' => [0x87, '‡'],
|
||
'circumflex (0x88)' => [0x88, 'ˆ'],
|
||
'per mille (0x89)' => [0x89, '‰'],
|
||
'S caron (0x8A)' => [0x8A, 'Š'],
|
||
'left single guillemet (0x8B)' => [0x8B, '‹'],
|
||
'OE ligature (0x8C)' => [0x8C, 'Œ'],
|
||
'Z caron (0x8E)' => [0x8E, 'Ž'],
|
||
'left single quote (0x91)' => [0x91, "\u{2018}"],
|
||
'right single quote (0x92)' => [0x92, "\u{2019}"],
|
||
'left double quote (0x93)' => [0x93, "\u{201C}"],
|
||
'right double quote (0x94)' => [0x94, "\u{201D}"],
|
||
'bullet (0x95)' => [0x95, '•'],
|
||
'en dash (0x96)' => [0x96, '–'],
|
||
'em dash (0x97)' => [0x97, '—'],
|
||
'tilde (0x98)' => [0x98, '˜'],
|
||
'trademark (0x99)' => [0x99, '™'],
|
||
's caron (0x9A)' => [0x9A, 'š'],
|
||
'right single guillemet (0x9B)' => [0x9B, '›'],
|
||
'oe ligature (0x9C)' => [0x9C, 'œ'],
|
||
'z caron (0x9E)' => [0x9E, 'ž'],
|
||
'Y diaeresis (0x9F)' => [0x9F, 'Ÿ'],
|
||
]);
|
||
|
||
it('converts Windows-1252 high bytes (0xA0-0xFF) as ISO-8859-1 identity', function (): void {
|
||
// Given
|
||
$input = chr(0xE9);
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'Windows-1252');
|
||
|
||
// Then the result is the UTF-8 encoded é
|
||
expect($result)->toBe('é');
|
||
});
|
||
|
||
it('converts a full Windows-1252 sentence with mixed ranges', function (): void {
|
||
// Given
|
||
$input = "B\x9Cuf \xE0 5\x80";
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'Windows-1252');
|
||
|
||
// Then
|
||
expect($result)->toBe('Bœuf à 5€');
|
||
});
|
||
|
||
it('handles undefined Windows-1252 bytes (0x81, 0x8D, 0x8F, 0x90, 0x9D) via Latin-1 fallback', function (int $byte): void {
|
||
// Given
|
||
$input = chr($byte);
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'Windows-1252');
|
||
|
||
// Then
|
||
expect(mb_check_encoding($result, 'UTF-8'))->toBeTrue()
|
||
->and(mb_strlen($result, 'UTF-8'))->toBe(1);
|
||
})->with([0x81, 0x8D, 0x8F, 0x90, 0x9D]);
|
||
|
||
it('converts ISO-8859-15 euro sign to UTF-8', function (): void {
|
||
// Given
|
||
$input = chr(0xA4);
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'ISO-8859-15');
|
||
|
||
// Then
|
||
expect($result)->toBe('€');
|
||
});
|
||
|
||
it('accepts encoding aliases', function (string $alias, string $expected): void {
|
||
// Given
|
||
$input = "caf\xE9"; // é in Latin-1/Win-1252
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, $alias);
|
||
|
||
// Then
|
||
expect($result)->toBe($expected);
|
||
})->with([
|
||
'latin1' => ['latin1', 'café'],
|
||
'latin-1' => ['latin-1', 'café'],
|
||
'win1252' => ['win1252', 'café'],
|
||
'windows1252' => ['windows1252', 'café'],
|
||
'cp1252' => ['cp1252', 'café'],
|
||
]);
|
||
|
||
it('throws ValueError for an unsupported encoding', function (): void {
|
||
// Given / When / Then
|
||
StringEncoder::toUtf8('hello', 'EBCDIC-42');
|
||
})->throws(\ValueError::class, 'Unsupported encoding');
|
||
});
|
||
|
||
describe('detectAndConvertToUtf8()', function (): void {
|
||
|
||
it('returns an empty string when given an empty string', function (): void {
|
||
// Given
|
||
$input = '';
|
||
|
||
// When
|
||
$result = StringEncoder::detectAndConvertToUtf8($input);
|
||
|
||
// Then
|
||
expect($result)->toBe('');
|
||
});
|
||
|
||
it('detects and passes through valid UTF-8 with a hint', function (): void {
|
||
// Given
|
||
$input = 'Héllo world 🌍';
|
||
|
||
// When
|
||
$result = StringEncoder::detectAndConvertToUtf8($input, 'Windows-1252');
|
||
|
||
// Then
|
||
expect($result)->toBe('Héllo world 🌍');
|
||
});
|
||
|
||
it('strips the UTF-8 BOM from auto-detected UTF-8 strings', function (): void {
|
||
// Given
|
||
$input = "\xEF\xBB\xBFBonjour";
|
||
|
||
// When
|
||
$result = StringEncoder::detectAndConvertToUtf8($input);
|
||
|
||
// Then
|
||
expect($result)->toBe('Bonjour');
|
||
});
|
||
|
||
it('uses encoding hints to guide detection', function (): void {
|
||
// Given
|
||
$input = "5\x80";
|
||
|
||
// When
|
||
$result = StringEncoder::detectAndConvertToUtf8($input, 'Windows-1252');
|
||
|
||
// Then
|
||
expect($result)->toBe('5€');
|
||
});
|
||
|
||
it('produces valid UTF-8 output regardless of detection result', function (): void {
|
||
// Given
|
||
$input = "\xFE\x01\xFE\x01";
|
||
|
||
// When / Then
|
||
try {
|
||
$result = StringEncoder::detectAndConvertToUtf8($input);
|
||
expect(mb_check_encoding($result, 'UTF-8'))->toBeTrue();
|
||
} catch (\RuntimeException $e) {
|
||
expect($e->getMessage())->toContain('Unable to detect encoding');
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('arrayToUtf8()', function (): void {
|
||
|
||
it('handles an empty array', function (): void {
|
||
// Given
|
||
$input = [];
|
||
|
||
// When
|
||
$result = StringEncoder::arrayToUtf8($input, 'UTF-8');
|
||
|
||
// Then
|
||
expect($result)->toBe([]);
|
||
});
|
||
|
||
it('converts an array of Windows-1252 strings with special characters', function (): void {
|
||
// Given
|
||
$input = ["5\x80", "B\x9Cuf"];
|
||
|
||
// When
|
||
$result = StringEncoder::arrayToUtf8($input, 'Windows-1252');
|
||
|
||
// Then
|
||
expect($result)->toBe(['5€', 'Bœuf']);
|
||
});
|
||
});
|
||
|
||
describe('supportedEncodings()', function (): void {
|
||
|
||
it('returns a non-empty list of strings', function (): void {
|
||
// Given / When
|
||
$result = StringEncoder::supportedEncodings();
|
||
|
||
// Then
|
||
expect($result)
|
||
->toBeArray()
|
||
->not->toBeEmpty()
|
||
->each->toBeString();
|
||
});
|
||
|
||
it('includes all canonical encodings', function (): void {
|
||
// Given
|
||
$expected = ['UTF-8', 'ISO-8859-15', 'Windows-1252'];
|
||
|
||
// When
|
||
$result = StringEncoder::supportedEncodings();
|
||
|
||
// Then
|
||
foreach ($expected as $encoding) {
|
||
expect($result)->toContain($encoding);
|
||
}
|
||
});
|
||
|
||
it('includes common aliases', function (): void {
|
||
// Given
|
||
$aliases = ['latin1', 'latin-1', 'latin9', 'cp1252', 'utf8', 'utf-8'];
|
||
|
||
// When
|
||
$result = StringEncoder::supportedEncodings();
|
||
|
||
// Then
|
||
foreach ($aliases as $alias) {
|
||
expect($result)->toContain($alias);
|
||
}
|
||
});
|
||
|
||
it('contains no duplicates', function (): void {
|
||
// Given
|
||
$result = StringEncoder::supportedEncodings();
|
||
|
||
// When / Then
|
||
expect($result)->toBe(array_values(array_unique($result)));
|
||
});
|
||
});
|
||
|
||
describe('encoding resolution (via toUtf8)', function (): void {
|
||
|
||
it('resolves case-insensitively', function (string $variant): void {
|
||
// Given
|
||
$input = 'hello';
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, $variant);
|
||
|
||
// Then
|
||
expect($result)->toBe('hello');
|
||
})->with(['utf-8', 'UTF-8', 'Utf-8', 'UTF8', 'utf8']);
|
||
|
||
it('trims whitespace around encoding names', function (): void {
|
||
// Given
|
||
$input = 'hello';
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, ' utf-8 ');
|
||
|
||
// Then
|
||
expect($result)->toBe('hello');
|
||
});
|
||
});
|
||
|
||
describe('regressions', function (): void {
|
||
|
||
it('correctly converts "Bœuf" from Windows-1252 (œ = 0x9C)', function (): void {
|
||
// Given
|
||
$input = "B\x9Cuf";
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'Windows-1252');
|
||
|
||
// Then
|
||
expect($result)->toBe('Bœuf')
|
||
->and(mb_check_encoding($result, 'UTF-8'))->toBeTrue();
|
||
});
|
||
|
||
it('round-trips UTF-8 → identity without corruption', function (): void {
|
||
// Given
|
||
$input = 'Héllo 世界 🎵 Ÿ œ € ™';
|
||
|
||
// When
|
||
$result = StringEncoder::toUtf8($input, 'UTF-8');
|
||
|
||
// Then
|
||
expect($result)->toBe($input);
|
||
});
|
||
});
|