Skip to content

Commit 4d5318a

Browse files
guillaumeVDPnicolas-grekas
authored andcommitted
[Console] Fix OUTPUT_RAW corrupting binary content on Windows
1 parent 7b1f1c3 commit 4d5318a

File tree

3 files changed

+56
-4
lines changed

3 files changed

+56
-4
lines changed

Output/ConsoleOutput.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,24 +142,54 @@ private function isRunningOS400(): bool
142142
*/
143143
private function openOutputStream()
144144
{
145+
static $stdout;
146+
147+
if ($stdout) {
148+
return $stdout;
149+
}
150+
145151
if (!$this->hasStdoutSupport()) {
146-
return fopen('php://output', 'w');
152+
return $stdout = fopen('php://output', 'w');
147153
}
148154

149155
// Use STDOUT when possible to prevent from opening too many file descriptors
150-
return \defined('STDOUT') ? \STDOUT : (@fopen('php://stdout', 'w') ?: fopen('php://output', 'w'));
156+
if (!\defined('STDOUT')) {
157+
return $stdout = @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
158+
}
159+
160+
// On Windows, STDOUT is opened in text mode; reopen in binary mode to prevent \n to \r\n conversion
161+
if ('\\' === \DIRECTORY_SEPARATOR) {
162+
return $stdout = @fopen('php://stdout', 'w') ?: \STDOUT;
163+
}
164+
165+
return $stdout = \STDOUT;
151166
}
152167

153168
/**
154169
* @return resource
155170
*/
156171
private function openErrorStream()
157172
{
173+
static $stderr;
174+
175+
if ($stderr) {
176+
return $stderr;
177+
}
178+
158179
if (!$this->hasStderrSupport()) {
159-
return fopen('php://output', 'w');
180+
return $stderr = fopen('php://output', 'w');
160181
}
161182

162183
// Use STDERR when possible to prevent from opening too many file descriptors
163-
return \defined('STDERR') ? \STDERR : (@fopen('php://stderr', 'w') ?: fopen('php://output', 'w'));
184+
if (!\defined('STDERR')) {
185+
return $stderr = @fopen('php://stderr', 'w') ?: fopen('php://output', 'w');
186+
}
187+
188+
// On Windows, STDERR is opened in text mode; reopen in binary mode to prevent \n → \r\n conversion
189+
if ('\\' === \DIRECTORY_SEPARATOR) {
190+
return $stderr = @fopen('php://stderr', 'w') ?: \STDERR;
191+
}
192+
193+
return $stderr ??= \STDERR;
164194
}
165195
}

Tests/Fixtures/binary_output.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
$vendor = __DIR__;
4+
while (!file_exists($vendor.'/vendor')) {
5+
$vendor = \dirname($vendor);
6+
}
7+
require $vendor.'/vendor/autoload.php';
8+
9+
use Symfony\Component\Console\Output\ConsoleOutput;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
12+
$output = new ConsoleOutput();
13+
$output->write("HELLO\nWORLD", false, OutputInterface::OUTPUT_RAW);

Tests/Output/StreamOutputTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Console\Output\Output;
1616
use Symfony\Component\Console\Output\StreamOutput;
17+
use Symfony\Component\Process\Process;
1718

1819
class StreamOutputTest extends TestCase
1920
{
@@ -65,4 +66,12 @@ public function testDoWriteOnFailure()
6566
rewind($output->getStream());
6667
$this->assertEquals('', stream_get_contents($output->getStream()));
6768
}
69+
70+
public function testRawOutputPreservesNewlinesInContent()
71+
{
72+
$process = new Process(['php', __DIR__.'/../Fixtures/binary_output.php']);
73+
$process->run();
74+
75+
$this->assertSame("HELLO\nWORLD", $process->getOutput(), 'Raw output must not convert LF in binary content');
76+
}
6877
}

0 commit comments

Comments
 (0)