Handle Surrogates If Read

It is probably very anal of me to do this. Excel does it. I can't see it happening in the wild.
This commit is contained in:
oleibman
2025-12-01 16:57:42 -08:00
parent 0a96e52457
commit 98ca50e9fe
3 changed files with 28 additions and 4 deletions
+18 -3
View File
@@ -305,7 +305,7 @@ class StringHelper
*/
public static function controlCharacterOOXML2PHP(string $textValue): string
{
return Preg::replaceCallback('/_x[0-9A-F]{4}_/', self::toOutChar(...), $textValue);
return Preg::replaceCallback('/_x[0-9A-F]{4}_(_xD[CDEF][0-9A-F]{2}_)?/', self::toOutChar(...), $textValue);
}
private static function toHexVal(string $char): int
@@ -322,12 +322,27 @@ class StringHelper
{
/** @var string */
$chars = $match[0];
$t = ((self::toHexVal($chars[2]) << 12)
$h = ((self::toHexVal($chars[2]) << 12)
| (self::toHexVal($chars[3]) << 8)
| (self::toHexVal($chars[4]) << 4)
| (self::toHexVal($chars[5])));
if (strlen($chars) === 7) { // no low surrogate
if ($chars[2] === 'D' && in_array($chars[3], ['8', '9', 'A', 'B', 'C', 'D', 'E', 'F'], true)) {
return mb_chr(0xFFFD, 'UTF-8');
}
return mb_chr($t, 'UTF-8');
return mb_chr($h, 'UTF-8');
}
if ($chars[2] !== 'D' || !in_array($chars[3], ['8', '9', 'A', 'B'], true)) {
return mb_chr($h, 'UTF-8') . mb_chr(0xFFFD, 'UTF-8');
}
$l = ((self::toHexVal($chars[9]) << 12)
| (self::toHexVal($chars[10]) << 8)
| (self::toHexVal($chars[11]) << 4)
| (self::toHexVal($chars[12])));
$result = 0x10000 + ($h - 0xD800) * 0x400 + ($l - 0xDC00);
return mb_chr($result, 'UTF-8');
}
/**
@@ -71,7 +71,7 @@ class UnderscoreTest extends AbstractFunctional
if ($data === false) {
self::fail('Unable to read file');
} else {
self::assertStringContainsString('count="4"', $data);
self::assertStringContainsString('count="7"', $data);
self::assertStringContainsString(
"<t>line_x000D_\nwith_x000D_\nbreaks</t>",
$data
@@ -82,6 +82,9 @@ class UnderscoreTest extends AbstractFunctional
$data
);
self::assertStringContainsString('<t>_xC1EF_</t>', $data);
self::assertStringContainsString('<t>_xD801__xDC05_</t>', $data);
self::assertStringContainsString('<t>_xD801__x0038_</t>', $data);
self::assertStringContainsString('<t>_x0039__xDC05_</t>', $data);
}
}
@@ -102,6 +105,12 @@ class UnderscoreTest extends AbstractFunctional
self::assertSame($expected, $sheet->getCell('A3')->getValue());
$expected = '쇯';
self::assertSame($expected, $sheet->getCell('A4')->getValue());
$expected = '𐐅';
self::assertSame($expected, $sheet->getCell('A5')->getValue(), 'outside BMP');
$expected = '8';
self::assertSame($expected, $sheet->getCell('A6')->getValue(), 'high surrogate without low');
$expected = '9';
self::assertSame($expected, $sheet->getCell('A7')->getValue(), 'low surrogate without high');
}
public function testX000dNotPreserved(): void
Binary file not shown.