diff --git a/src/Composer/Json/JsonFormatter.php b/src/Composer/Json/JsonFormatter.php index 680a57baf..44acaff59 100644 --- a/src/Composer/Json/JsonFormatter.php +++ b/src/Composer/Json/JsonFormatter.php @@ -69,6 +69,13 @@ class JsonFormatter $l = strlen($match[1]); if ($l % 2) { + $code = hexdec($match[2]); + // 0xD800..0xDFFF denotes UTF-16 surrogate pair which won't be unescaped + // see https://github.com/composer/composer/issues/7510 + if (0xD800 <= $code && 0xDFFF >= $code) { + return $match[0]; + } + return str_repeat('\\', $l - 1) . mb_convert_encoding( pack('H*', $match[2]), 'UTF-8', diff --git a/tests/Composer/Test/Json/JsonFormatterTest.php b/tests/Composer/Test/Json/JsonFormatterTest.php index 7be8fafe2..fc6cf53ed 100644 --- a/tests/Composer/Test/Json/JsonFormatterTest.php +++ b/tests/Composer/Test/Json/JsonFormatterTest.php @@ -33,6 +33,20 @@ class JsonFormatterTest extends TestCase $this->assertEquals($expected, $this->getCharacterCodes($encodedData)); } + /** + * Surrogate pairs are intentionally skipped and not unescaped + * https://github.com/composer/composer/issues/7510 + */ + public function testUtf16SurrogatePair() + { + if (!extension_loaded('mbstring')) { + $this->markTestSkipped('Test requires the mbstring extension'); + } + + $escaped = '"\ud83d\ude00"'; + $this->assertEquals($escaped, JsonFormatter::format($escaped, true, true)); + } + /** * Convert string to character codes split by a plus sign * @param string $string