diff --git a/src/Symfony/Component/String/ByteString.php b/src/Symfony/Component/String/ByteString.php index a248a5b75e..6c1bcc9e7f 100644 --- a/src/Symfony/Component/String/ByteString.php +++ b/src/Symfony/Component/String/ByteString.php @@ -97,7 +97,7 @@ class ByteString extends AbstractString $suffix = (string) $suffix; } - return \strlen($this->string) - \strlen($suffix) === ($this->ignoreCase ? strripos($this->string, $suffix) : strrpos($this->string, $suffix)); + return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase); } public function equalsTo($string): bool @@ -362,7 +362,7 @@ class ByteString extends AbstractString return parent::startsWith($prefix); } - return '' !== $prefix && 0 === ($this->ignoreCase ? stripos($this->string, $prefix) : strpos($this->string, $prefix)); + return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix))); } public function title(bool $allWords = false): parent diff --git a/src/Symfony/Component/String/CodePointString.php b/src/Symfony/Component/String/CodePointString.php index e0ef21e014..6aeac3d817 100644 --- a/src/Symfony/Component/String/CodePointString.php +++ b/src/Symfony/Component/String/CodePointString.php @@ -93,7 +93,7 @@ class CodePointString extends AbstractUnicodeString return preg_match('{'.preg_quote($suffix).'$}iuD', $this->string); } - return \strlen($this->string) - \strlen($suffix) === strrpos($this->string, $suffix); + return \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix)); } public function equalsTo($string): bool @@ -107,7 +107,7 @@ class CodePointString extends AbstractUnicodeString } if ('' !== $string && $this->ignoreCase) { - return mb_strlen($string, 'UTF-8') === mb_strlen($this->string, 'UTF-8') && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); + return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); } return $string === $this->string; @@ -256,6 +256,10 @@ class CodePointString extends AbstractUnicodeString return false; } - return 0 === ($this->ignoreCase ? mb_stripos($this->string, $prefix, 0, 'UTF-8') : strpos($this->string, $prefix)); + if ($this->ignoreCase) { + return 0 === mb_stripos($this->string, $prefix, 0, 'UTF-8'); + } + + return 0 === strncmp($this->string, $prefix, \strlen($prefix)); } } diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php index b16bf8cc58..1e7eb4dccb 100644 --- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php @@ -859,9 +859,12 @@ abstract class AbstractAsciiTestCase extends TestCase /** * @dataProvider provideStartsWith */ - public function testStartsWith(bool $expected, string $origin, $prefix) + public function testStartsWith(bool $expected, string $origin, $prefix, int $form = null) { - $this->assertSame($expected, static::createFromString($origin)->startsWith($prefix)); + $instance = static::createFromString($origin); + $instance = $form ? $instance->normalize($form) : $instance; + + $this->assertSame($expected, $instance->startsWith($prefix)); } public static function provideStartsWith() @@ -910,9 +913,12 @@ abstract class AbstractAsciiTestCase extends TestCase /** * @dataProvider provideEndsWith */ - public function testEndsWith(bool $expected, string $origin, $suffix) + public function testEndsWith(bool $expected, string $origin, $suffix, int $form = null) { - $this->assertSame($expected, static::createFromString($origin)->endsWith($suffix)); + $instance = static::createFromString($origin); + $instance = $form ? $instance->normalize($form) : $instance; + + $this->assertSame($expected, $instance->endsWith($suffix)); } public static function provideEndsWith() diff --git a/src/Symfony/Component/String/Tests/UnicodeStringTest.php b/src/Symfony/Component/String/Tests/UnicodeStringTest.php index 7d9c877b44..f37476f6ce 100644 --- a/src/Symfony/Component/String/Tests/UnicodeStringTest.php +++ b/src/Symfony/Component/String/Tests/UnicodeStringTest.php @@ -200,4 +200,26 @@ class UnicodeStringTest extends AbstractUnicodeTestCase ] ); } + + public static function provideStartsWith() + { + return array_merge( + parent::provideStartsWith(), + [ + [false, "cle\u{0301} prive\u{0301}e", 'cle', UnicodeString::NFD], + [true, "cle\u{0301} prive\u{0301}e", 'clé', UnicodeString::NFD], + ] + ); + } + + public static function provideEndsWith() + { + return array_merge( + parent::provideEndsWith(), + [ + [false, "cle\u{0301} prive\u{0301}e", 'ee', UnicodeString::NFD], + [true, "cle\u{0301} prive\u{0301}e", 'ée', UnicodeString::NFD], + ] + ); + } } diff --git a/src/Symfony/Component/String/UnicodeString.php b/src/Symfony/Component/String/UnicodeString.php index f81a9ba3ec..61e8c008dc 100644 --- a/src/Symfony/Component/String/UnicodeString.php +++ b/src/Symfony/Component/String/UnicodeString.php @@ -97,11 +97,15 @@ class UnicodeString extends AbstractUnicodeString $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($suffix, $form) ?: $suffix = normalizer_normalize($suffix, $form); - if ('' === $suffix || false === $suffix || false === $i = $this->ignoreCase ? grapheme_strripos($this->string, $suffix) : grapheme_strrpos($this->string, $suffix)) { + if ('' === $suffix || false === $suffix) { return false; } - return grapheme_strlen($this->string) - grapheme_strlen($suffix) === $i; + if ($this->ignoreCase) { + return 0 === mb_stripos(grapheme_extract($this->string, \strlen($suffix), GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)), $suffix, 0, 'UTF-8'); + } + + return $suffix === grapheme_extract($this->string, \strlen($suffix), GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)); } public function equalsTo($string): bool @@ -118,7 +122,7 @@ class UnicodeString extends AbstractUnicodeString normalizer_is_normalized($string, $form) ?: $string = normalizer_normalize($string, $form); if ('' !== $string && false !== $string && $this->ignoreCase) { - return grapheme_strlen($string) === grapheme_strlen($this->string) && 0 === grapheme_stripos($this->string, $string); + return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); } return $string === $this->string; @@ -332,7 +336,15 @@ class UnicodeString extends AbstractUnicodeString $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($prefix, $form) ?: $prefix = normalizer_normalize($prefix, $form); - return '' !== $prefix && false !== $prefix && 0 === ($this->ignoreCase ? grapheme_stripos($this->string, $prefix) : grapheme_strpos($this->string, $prefix)); + if ('' === $prefix || false === $prefix) { + return false; + } + + if ($this->ignoreCase) { + return 0 === mb_stripos(grapheme_extract($this->string, \strlen($prefix), GRAPHEME_EXTR_MAXBYTES), $prefix, 0, 'UTF-8'); + } + + return $prefix === grapheme_extract($this->string, \strlen($prefix), GRAPHEME_EXTR_MAXBYTES); } public function __clone()