From 90087b4fb37b06d21f17325165e93fb9295b800a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 2 Nov 2021 11:36:31 +0100 Subject: [PATCH] Fix handling of php bin proxies combined with declare() on php <8, fixes #10246 (#10249) --- src/Composer/Installer/BinaryInstaller.php | 111 +++++++++++--- .../Test/Installer/BinaryInstallerTest.php | 136 ++++++++++++++++++ 2 files changed, 227 insertions(+), 20 deletions(-) create mode 100644 tests/Composer/Test/Installer/BinaryInstallerTest.php diff --git a/src/Composer/Installer/BinaryInstaller.php b/src/Composer/Installer/BinaryInstaller.php index d4470f4f0..91be81b54 100644 --- a/src/Composer/Installer/BinaryInstaller.php +++ b/src/Composer/Installer/BinaryInstaller.php @@ -255,17 +255,13 @@ class BinaryInstaller $binContents = file_get_contents($bin); // For php files, we generate a PHP proxy instead of a shell one, // which allows calling the proxy with a custom php process - if (preg_match('{^(?:#!(?:/usr)?/bin/env php|#!(?:/usr)?/bin/php|handle = fopen(\$opened_path, \$mode); + \$this->position = 0; + + // remove all traces of this stream wrapper once it has been used + stream_wrapper_unregister('composer-bin-proxy'); + + return (bool) \$this->handle; + } + + public function stream_read(\$count) + { + \$data = fread(\$this->handle, \$count); + + if (\$this->position === 0) { + \$data = preg_replace('{^#!.*\\r?\\n}', '', \$data); + } + + \$this->position += strlen(\$data); + + return \$data; + } + + public function stream_cast(\$castAs) + { + return \$this->handle; + } + + public function stream_close() + { + fclose(\$this->handle); + } + + public function stream_lock(\$operation) + { + return \$operation ? flock(\$this->handle, \$operation) : true; + } + + public function stream_tell() + { + return \$this->position; + } + + public function stream_eof() + { + return feof(\$this->handle); + } + + public function stream_stat() + { + return fstat(\$this->handle); + } + + public function stream_set_option(\$option, \$arg1, \$arg2) + { + return true; } } + } - $proxyCode = << /dev/null; cd $binDir && pwd) @@ -305,7 +378,5 @@ fi "\${dir}/$binFile" "\$@" PROXY; - - return $proxyCode; } } diff --git a/tests/Composer/Test/Installer/BinaryInstallerTest.php b/tests/Composer/Test/Installer/BinaryInstallerTest.php new file mode 100644 index 000000000..b862556de --- /dev/null +++ b/tests/Composer/Test/Installer/BinaryInstallerTest.php @@ -0,0 +1,136 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Installer; + +use Composer\Installer\BinaryInstaller; +use Composer\Util\Filesystem; +use Composer\Test\TestCase; +use Composer\Composer; +use Composer\Config; +use Composer\Util\ProcessExecutor; + +class BinaryInstallerTest extends TestCase +{ + /** + * @var string + */ + protected $rootDir; + + /** + * @var string + */ + protected $vendorDir; + + /** + * @var string + */ + protected $binDir; + + /** + * @var \Composer\IO\IOInterface&\PHPUnit\Framework\MockObject\MockObject + */ + protected $io; + + /** + * @var \Composer\Util\Filesystem + */ + protected $fs; + + protected function setUp() + { + $this->fs = new Filesystem; + + $this->rootDir = $this->getUniqueTmpDirectory(); + $this->vendorDir = $this->rootDir.DIRECTORY_SEPARATOR.'vendor'; + $this->ensureDirectoryExistsAndClear($this->vendorDir); + + $this->binDir = $this->rootDir.DIRECTORY_SEPARATOR.'bin'; + $this->ensureDirectoryExistsAndClear($this->binDir); + + $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + } + + protected function tearDown() + { + $this->fs->removeDirectory($this->rootDir); + } + + /** + * @dataProvider executableBinaryProvider + * @param string $contents + */ + public function testInstallAndExecBinaryWithFullCompat($contents) + { + $package = $this->createPackageMock(); + $package->expects($this->any()) + ->method('getBinaries') + ->willReturn(array('binary')); + + $this->ensureDirectoryExistsAndClear($this->vendorDir.'/foo/bar'); + file_put_contents($this->vendorDir.'/foo/bar/binary', $contents); + + $installer = new BinaryInstaller($this->io, $this->binDir, 'full', $this->fs); + $installer->installBinaries($package, $this->vendorDir.'/foo/bar'); + + $proc = new ProcessExecutor(); + $proc->execute($this->binDir.'/binary arg', $output); + $this->assertEquals('', $proc->getErrorOutput()); + $this->assertEquals('success arg', $output); + } + + public function executableBinaryProvider() + { + $tests = array( + 'simple php file' => array(<<<'EOL' + array(<<<'EOL' +#!/usr/bin/env php + array( + base64_decode('IyEvdXNyL2Jpbi9lbnYgcGhwCjw/cGhwCgpQaGFyOjptYXBQaGFyKCd0ZXN0LnBoYXInKTsKCnJlcXVpcmUgJ3BoYXI6Ly90ZXN0LnBoYXIvcnVuLnBocCc7CgpfX0hBTFRfQ09NUElMRVIoKTsgPz4NCj4AAAABAAAAEQAAAAEACQAAAHRlc3QucGhhcgAAAAAHAAAAcnVuLnBocCoAAADb9n9hKgAAAMUDDWGkAQAAAAAAADw/cGhwIGVjaG8gInN1Y2Nlc3MgIi4kX1NFUlZFUlsiYXJndiJdWzFdO1SOC0IE3+UN0yzrHIwyspp9slhmAgAAAEdCTUI=') + ), + ); + + if (PHP_VERSION_ID >= 70000) { + $tests += array( + 'shebang with strict types declare' => array(<<<'EOL' +#!/usr/bin/env php +getMockBuilder('Composer\Package\Package') + ->setConstructorArgs(array(md5((string) mt_rand()), '1.0.0.0', '1.0.0')) + ->getMock(); + } +}