Skip to content

Commit 218ca62

Browse files
committed
[Console][Table] Fix invalid UTF-8 due to text wrapping
Fixes symfony#58286
1 parent 1be4761 commit 218ca62

File tree

3 files changed

+17
-6
lines changed

3 files changed

+17
-6
lines changed

src/Symfony/Component/Console/Formatter/OutputFormatter.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Console\Formatter;
1313

1414
use Symfony\Component\Console\Exception\InvalidArgumentException;
15+
use Symfony\Component\Console\Helper\Helper;
1516

1617
use function Symfony\Component\String\b;
1718

@@ -146,9 +147,11 @@ public function formatAndWrap(?string $message, int $width)
146147
continue;
147148
}
148149

150+
// convert byte position to character position.
151+
$pos = Helper::length(substr($message, 0, $pos));
149152
// add the text up to the next tag
150-
$output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
151-
$offset = $pos + \strlen($text);
153+
$output .= $this->applyCurrentStyle(Helper::substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
154+
$offset = $pos + Helper::length($text);
152155

153156
// opening tag?
154157
if ($open = '/' !== $text[1]) {
@@ -169,7 +172,7 @@ public function formatAndWrap(?string $message, int $width)
169172
}
170173
}
171174

172-
$output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
175+
$output .= $this->applyCurrentStyle(Helper::substr($message, $offset), $output, $width, $currentLineLength);
173176

174177
return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
175178
}
@@ -236,8 +239,8 @@ private function applyCurrentStyle(string $text, string $current, int $width, in
236239
}
237240

238241
if ($currentLineLength) {
239-
$prefix = substr($text, 0, $i = $width - $currentLineLength)."\n";
240-
$text = substr($text, $i);
242+
$prefix = Helper::substr($text, 0, $i = $width - $currentLineLength)."\n";
243+
$text = Helper::substr($text, $i);
241244
} else {
242245
$prefix = '';
243246
}
@@ -253,7 +256,7 @@ private function applyCurrentStyle(string $text, string $current, int $width, in
253256
$lines = explode("\n", $text);
254257

255258
foreach ($lines as $line) {
256-
$currentLineLength += \strlen($line);
259+
$currentLineLength += Helper::length($line);
257260
if ($width <= $currentLineLength) {
258261
$currentLineLength = 0;
259262
}

src/Symfony/Component/Console/Helper/Helper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ public static function substr(?string $string, int $from, ?int $length = null):
8686
{
8787
$string ??= '';
8888

89+
if (preg_match('//u', $string)) {
90+
return (new UnicodeString($string))->slice($from, $length);
91+
}
92+
8993
if (false === $encoding = mb_detect_encoding($string, null, true)) {
9094
return substr($string, $from, $length);
9195
}

src/Symfony/Component/Console/Tests/Formatter/OutputFormatterTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ public function testFormatAndWrap()
365365
$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 <error>ipsum</error> dolor <info>sit</info> amet', 4));
366366
$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 <error>ipsum</error> dolor <info>sit</info> amet', 8));
367367
$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 <error>ipsum</error> dolor <info>sit</info>, <error>amet</error> et <info>laudantium</info> architecto', 18));
368+
$this->assertSame("\e[37;41mnon-empty-array\e[39;49m\e[37;41m<mixed, mixed>\e[39;49m given.\n🪪\n argument.type", $formatter->formatAndWrap("<error>non-empty-array<mixed, mixed></error> given.\n🪪 argument.type", 38));
369+
$this->assertSame("Usuário <strong>{{user_name}}</strong> não é válid\no.", $formatter->formatAndWrap('Usuário <strong>{{user_name}}</strong> não é válido.', 50));
368370

369371
$formatter = new OutputFormatter();
370372

@@ -376,6 +378,8 @@ public function testFormatAndWrap()
376378
$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));
377379
$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));
378380
$this->assertSame('', $formatter->formatAndWrap(null, 5));
381+
$this->assertSame("non-empty-array<mixed, mixed> given.\n🪪\n argument.type", $formatter->formatAndWrap("<error>non-empty-array<mixed, mixed></error> given.\n🪪 argument.type", 38));
382+
$this->assertSame("Usuário <strong>{{user_name}}</strong> não é válid\no.", $formatter->formatAndWrap('Usuário <strong>{{user_name}}</strong> não é válido.', 50));
379383
}
380384
}
381385

0 commit comments

Comments
 (0)