From 218ca62bb017ab74de154c22f3ace6cc4e118c92 Mon Sep 17 00:00:00 2001 From: schlndh Date: Sat, 26 Jul 2025 10:03:12 +0200 Subject: [PATCH] [Console][Table] Fix invalid UTF-8 due to text wrapping Fixes #58286 --- .../Console/Formatter/OutputFormatter.php | 15 +++++++++------ src/Symfony/Component/Console/Helper/Helper.php | 4 ++++ .../Tests/Formatter/OutputFormatterTest.php | 4 ++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatter.php b/src/Symfony/Component/Console/Formatter/OutputFormatter.php index 52d831620069c..15c14b6d86348 100644 --- a/src/Symfony/Component/Console/Formatter/OutputFormatter.php +++ b/src/Symfony/Component/Console/Formatter/OutputFormatter.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Console\Formatter; use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Helper\Helper; use function Symfony\Component\String\b; @@ -146,9 +147,11 @@ public function formatAndWrap(?string $message, int $width) continue; } + // convert byte position to character position. + $pos = Helper::length(substr($message, 0, $pos)); // add the text up to the next tag - $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength); - $offset = $pos + \strlen($text); + $output .= $this->applyCurrentStyle(Helper::substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength); + $offset = $pos + Helper::length($text); // opening tag? if ($open = '/' !== $text[1]) { @@ -169,7 +172,7 @@ public function formatAndWrap(?string $message, int $width) } } - $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength); + $output .= $this->applyCurrentStyle(Helper::substr($message, $offset), $output, $width, $currentLineLength); return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']); } @@ -236,8 +239,8 @@ private function applyCurrentStyle(string $text, string $current, int $width, in } if ($currentLineLength) { - $prefix = substr($text, 0, $i = $width - $currentLineLength)."\n"; - $text = substr($text, $i); + $prefix = Helper::substr($text, 0, $i = $width - $currentLineLength)."\n"; + $text = Helper::substr($text, $i); } else { $prefix = ''; } @@ -253,7 +256,7 @@ private function applyCurrentStyle(string $text, string $current, int $width, in $lines = explode("\n", $text); foreach ($lines as $line) { - $currentLineLength += \strlen($line); + $currentLineLength += Helper::length($line); if ($width <= $currentLineLength) { $currentLineLength = 0; } diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index eec3284988c0d..468d06689e91f 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -86,6 +86,10 @@ public static function substr(?string $string, int $from, ?int $length = null): { $string ??= ''; + if (preg_match('//u', $string)) { + return (new UnicodeString($string))->slice($from, $length); + } + if (false === $encoding = mb_detect_encoding($string, null, true)) { return substr($string, $from, $length); } diff --git a/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php index eb8e483f3033b..4dbeb702d0b08 100644 --- a/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php +++ b/src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php @@ -365,6 +365,8 @@ public function testFormatAndWrap() $this->assertSame("Lore\nm \e[37;41mip\e[39;49m\n\e[37;41msum\e[39;49m \ndolo\nr \e[32msi\e[39m\n\e[32mt\e[39m am\net", $formatter->formatAndWrap('Lorem ipsum dolor sit amet', 4)); $this->assertSame("Lorem \e[37;41mip\e[39;49m\n\e[37;41msum\e[39;49m dolo\nr \e[32msit\e[39m am\net", $formatter->formatAndWrap('Lorem ipsum dolor sit amet', 8)); $this->assertSame("Lorem \e[37;41mipsum\e[39;49m dolor \e[32m\e[39m\n\e[32msit\e[39m, \e[37;41mamet\e[39;49m et \e[32mlauda\e[39m\n\e[32mntium\e[39m architecto", $formatter->formatAndWrap('Lorem ipsum dolor sit, amet et laudantium architecto', 18)); + $this->assertSame("\e[37;41mnon-empty-array\e[39;49m\e[37;41m\e[39;49m given.\n🪪\n argument.type", $formatter->formatAndWrap("non-empty-array given.\n🪪 argument.type", 38)); + $this->assertSame("Usuário {{user_name}} não é válid\no.", $formatter->formatAndWrap('Usuário {{user_name}} não é válido.', 50)); $formatter = new OutputFormatter(); @@ -376,6 +378,8 @@ public function testFormatAndWrap() $this->assertSame("Â rèälly\nlöng tîtlè\nthät cöüld\nnèêd\nmúltîplê\nlínès", $formatter->formatAndWrap('Â rèälly löng tîtlè thät cöüld nèêd múltîplê línès', 10)); $this->assertSame("Â rèälly\nlöng tîtlè\nthät cöüld\nnèêd\nmúltîplê\n línès", $formatter->formatAndWrap("Â rèälly löng tîtlè thät cöüld nèêd múltîplê\n línès", 10)); $this->assertSame('', $formatter->formatAndWrap(null, 5)); + $this->assertSame("non-empty-array given.\n🪪\n argument.type", $formatter->formatAndWrap("non-empty-array given.\n🪪 argument.type", 38)); + $this->assertSame("Usuário {{user_name}} não é válid\no.", $formatter->formatAndWrap('Usuário {{user_name}} não é válido.', 50)); } }