From 2ab3d1e26890acee1c2b0ba8335ca7eed308db60 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 1 May 2025 21:34:14 -0700 Subject: [PATCH 01/40] use `Composer\Util\Platform::realpath()` --- src/Composer/Autoload/AutoloadGenerator.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index b86c3b60753b..2a9af24096a2 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -214,15 +214,15 @@ public function dump(Config $config, InstalledRepositoryInterface $localRepo, Ro // Do not remove double realpath() calls. // Fixes failing Windows realpath() implementation. // See https://bugs.php.net/bug.php?id=72738 - $basePath = $filesystem->normalizePath(realpath(realpath(Platform::getCwd()))); - $vendorPath = $filesystem->normalizePath(realpath(realpath($config->get('vendor-dir')))); + $basePath = $filesystem->normalizePath(Platform::realpath(Platform::realpath(Platform::getCwd()))); + $vendorPath = $filesystem->normalizePath(Platform::realpath(Platform::realpath($config->get('vendor-dir')))); $useGlobalIncludePath = $config->get('use-include-path'); $prependAutoloader = $config->get('prepend-autoloader') === false ? 'false' : 'true'; $targetDir = $vendorPath.'/'.$targetDir; $filesystem->ensureDirectoryExists($targetDir); - $vendorPathCode = $filesystem->findShortestPathCode(realpath($targetDir), $vendorPath, true); - $vendorPathToTargetDirCode = $filesystem->findShortestPathCode($vendorPath, realpath($targetDir), true); + $vendorPathCode = $filesystem->findShortestPathCode(Platform::realpath($targetDir), $vendorPath, true); + $vendorPathToTargetDirCode = $filesystem->findShortestPathCode($vendorPath, Platform::realpath($targetDir), true); $appBaseDirCode = $filesystem->findShortestPathCode($vendorPath, $basePath, true); $appBaseDirCode = str_replace('__DIR__', '$vendorDir', $appBaseDirCode); @@ -493,7 +493,7 @@ private function buildExclusionRegex(string $dir, array $excluded): ?string // if $dir does not exist, it should anyway not find anything there so no trouble if (file_exists($dir)) { // transform $dir in the same way that exclude-from-classmap patterns are transformed so we can match them against each other - $dirMatch = preg_quote(strtr(realpath($dir), '\\', '/')); + $dirMatch = preg_quote(strtr(Platform::realpath($dir), '\\', '/')); foreach ($excluded as $index => $pattern) { // extract the constant string prefix of the pattern here, until we reach a non-escaped regex special character $pattern = Preg::replace('{^(([^.+*?\[^\]$(){}=!<>|:\\\\#-]+|\\\\[.+*?\[^\]$(){}=!<>|:#-])*).*}', '$1', $pattern); @@ -1165,10 +1165,10 @@ class ComposerStaticInit$suffix $filesystem = new Filesystem(); - $vendorPathCode = ' => ' . $filesystem->findShortestPathCode(realpath($targetDir), $vendorPath, true, true) . " . '/"; - $vendorPharPathCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode(realpath($targetDir), $vendorPath, true, true) . " . '/"; - $appBaseDirCode = ' => ' . $filesystem->findShortestPathCode(realpath($targetDir), $basePath, true, true) . " . '/"; - $appBaseDirPharCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode(realpath($targetDir), $basePath, true, true) . " . '/"; + $vendorPathCode = ' => ' . $filesystem->findShortestPathCode(Platform::realpath($targetDir), $vendorPath, true, true) . " . '/"; + $vendorPharPathCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode(Platform::realpath($targetDir), $vendorPath, true, true) . " . '/"; + $appBaseDirCode = ' => ' . $filesystem->findShortestPathCode(Platform::realpath($targetDir), $basePath, true, true) . " . '/"; + $appBaseDirPharCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode(Platform::realpath($targetDir), $basePath, true, true) . " . '/"; $absoluteVendorPathCode = ' => ' . substr(var_export(rtrim($vendorDir, '\\/') . '/', true), 0, -1); $absoluteVendorPharPathCode = ' => ' . substr(var_export(rtrim('phar://' . $vendorDir, '\\/') . '/', true), 0, -1); @@ -1291,7 +1291,7 @@ static function ($matches) use (&$updir): string { $installPath = strtr(Platform::getCwd(), '\\', '/'); } - $resolvedPath = realpath($installPath . '/' . $updir); + $resolvedPath = Platform::realpath($installPath . '/' . $updir); if (false === $resolvedPath) { continue; } From 708c809e72acd64952868725045f4935312120fb Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 1 May 2025 21:36:02 -0700 Subject: [PATCH 02/40] use `Composer\Util\Platform::realpath()` --- src/Composer/Factory.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 5399899043f0..c6a2759c03bf 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -324,14 +324,14 @@ public function createComposer(IOInterface $io, $localConfig = null, $disablePlu // Load config and override with local config/auth config $config = static::createConfig($io, $cwd); - $isGlobal = $localConfigSource !== Config::SOURCE_UNKNOWN && realpath($config->get('home')) === realpath(dirname($localConfigSource)); + $isGlobal = $localConfigSource !== Config::SOURCE_UNKNOWN && Platform::realpath($config->get('home')) === Platform::realpath(dirname($localConfigSource)); $config->merge($localConfig, $localConfigSource); if (isset($composerFile)) { - $io->writeError('Loading config file ' . $composerFile .' ('.realpath($composerFile).')', true, IOInterface::DEBUG); - $config->setConfigSource(new JsonConfigSource(new JsonFile(realpath($composerFile), null, $io))); + $io->writeError('Loading config file ' . $composerFile .' ('.Platform::realpath($composerFile).')', true, IOInterface::DEBUG); + $config->setConfigSource(new JsonConfigSource(new JsonFile(Platform::realpath($composerFile), null, $io))); - $localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json', null, $io); + $localAuthFile = new JsonFile(dirname(Platform::realpath($composerFile)) . '/auth.json', null, $io); if ($localAuthFile->exists()) { $io->writeError('Loading config file ' . $localAuthFile->getPath(), true, IOInterface::DEBUG); self::validateJsonSchema($io, $localAuthFile, JsonFile::AUTH_SCHEMA); From e8a516fa2c4497b3abb0b4cb36d3da26349e980d Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 1 May 2025 21:44:22 -0700 Subject: [PATCH 03/40] Always treat stream wrappers as absolute paths https://github.com/composer/class-map-generator/pull/18/ --- src/Composer/Util/Filesystem.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 57e4fd6f3532..b2763f675c5f 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -29,9 +29,15 @@ class Filesystem /** @var ?ProcessExecutor */ private $processExecutor; + /** + * @var non-empty-string + */ + private $streamWrappersRegex; + public function __construct(?ProcessExecutor $executor = null) { $this->processExecutor = $executor; + $this->streamWrappersRegex = sprintf('{^(?:%s)://}', implode('|', array_map('preg_quote', stream_get_wrappers()))); } /** @@ -564,7 +570,10 @@ public function findShortestPathCode(string $from, string $to, bool $directories */ public function isAbsolutePath(string $path) { - return strpos($path, '/') === 0 || substr($path, 1, 1) === ':' || strpos($path, '\\\\') === 0; + return strpos($path, '/') === 0 + || substr($path, 1, 1) === ':' + || strpos($path, '\\\\') === 0 + || Preg::isMatch($this->streamWrappersRegex, $path); } /** From 198a7ea81bcbb13aec1d16f6c34af5fa7290eae1 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 1 May 2025 22:05:38 -0700 Subject: [PATCH 04/40] Remove `false` check which no longer can evaluate to false --- src/Composer/Autoload/AutoloadGenerator.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 2a9af24096a2..d7030c10823b 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -1292,9 +1292,7 @@ static function ($matches) use (&$updir): string { } $resolvedPath = Platform::realpath($installPath . '/' . $updir); - if (false === $resolvedPath) { - continue; - } + $autoloads[] = preg_quote(strtr($resolvedPath, '\\', '/')) . '/' . $path . '($|/)'; continue; } From 9f03ebaa10a391abcaf4d52da229792171662dea Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Fri, 2 May 2025 00:25:07 -0700 Subject: [PATCH 05/40] Add `::testIsAbsolutePath()` --- tests/Composer/Test/Util/FilesystemTest.php | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index e947e4fe9e03..efeda35c06a4 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -399,4 +399,32 @@ public function testCopyThenRemove(): void self::assertDirectoryDoesNotExist($this->workingDir . '/foo/bar', 'Still a directory: ' . $this->workingDir . '/foo/bar'); self::assertDirectoryDoesNotExist($this->workingDir . '/foo', 'Still a directory: ' . $this->workingDir . '/foo'); } + + /** + * A Unix path is absolute if it starts with a forward-slash. + * A Windows path is absolute if it starts with a drive letter followed by a colon and a back-slash. + * A network path is always absolute and starts with two backslashes. + * A stream path is always absolute and starts with a scheme followed by "://". + */ + public static function isAbsolutePathDataProvider(): array + { + return [ + 'unixPath' => ['/foo/bar', true], + 'smbPath' => ['\\\\smb\\folder\\file.txt', true], + 'windowsPath' => ['C:\\foo\\bar', true], + 'streamPath' => ['file://path/to/whatever', true], + 'relativeSubPath' => ['foo/bar', false], + 'relativeSubPath2' => ['./foo/bar', false], + 'relativeParentPath' => ['../foo/bar', false], + ]; + } + + /** + * @covers \Composer\Util\Filesystem::isAbsolutePath + * @dataProvider isAbsolutePathDataProvider + */ + public function testIsAbsolutePath(string $path, bool $expected): void + { + self::assertEquals($expected, (new Filesystem())->isAbsolutePath($path)); + } } From ed968c0bdc94d10a24c9537f9de8de5eb72db6a2 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Mon, 12 May 2025 21:52:22 -0700 Subject: [PATCH 06/40] Do not treat `file://` paths as absolute path without check it un-prefixed --- src/Composer/Util/Filesystem.php | 2 ++ tests/Composer/Test/Util/FilesystemTest.php | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index b2763f675c5f..83302cb9bed3 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -570,6 +570,8 @@ public function findShortestPathCode(string $from, string $to, bool $directories */ public function isAbsolutePath(string $path) { + $path = str_replace('file://', '', $path); + return strpos($path, '/') === 0 || substr($path, 1, 1) === ':' || strpos($path, '\\\\') === 0 diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index efeda35c06a4..6ae1e4025818 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -412,7 +412,9 @@ public static function isAbsolutePathDataProvider(): array 'unixPath' => ['/foo/bar', true], 'smbPath' => ['\\\\smb\\folder\\file.txt', true], 'windowsPath' => ['C:\\foo\\bar', true], - 'streamPath' => ['file://path/to/whatever', true], + 'streamPath' => ['customstreamwrapper://path/to/whatever', true], + 'fileStreamAbsolutePath' => ['file:///path/to/whatever', true], + 'fileStreamRelativePath' => ['file://path/to/whatever', false], 'relativeSubPath' => ['foo/bar', false], 'relativeSubPath2' => ['./foo/bar', false], 'relativeParentPath' => ['../foo/bar', false], @@ -425,6 +427,10 @@ public static function isAbsolutePathDataProvider(): array */ public function testIsAbsolutePath(string $path, bool $expected): void { + if (str_starts_with($path, 'customstreamwrapper://') && !in_array('customstreamwrapper', stream_get_wrappers(), true)) { + stream_wrapper_register('customstreamwrapper', __CLASS__); + } + self::assertEquals($expected, (new Filesystem())->isAbsolutePath($path)); } } From 27fac4b2a686cadd0026b20412c804af7227434b Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Mon, 12 May 2025 22:08:12 -0700 Subject: [PATCH 07/40] Recursively call `::realpath()` until `$path` does not change --- src/Composer/Autoload/AutoloadGenerator.php | 7 ++----- src/Composer/Util/Platform.php | 6 ++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index d7030c10823b..46e0f2df63ee 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -211,11 +211,8 @@ public function dump(Config $config, InstalledRepositoryInterface $localRepo, Ro $filesystem = new Filesystem(); $filesystem->ensureDirectoryExists($config->get('vendor-dir')); - // Do not remove double realpath() calls. - // Fixes failing Windows realpath() implementation. - // See https://bugs.php.net/bug.php?id=72738 - $basePath = $filesystem->normalizePath(Platform::realpath(Platform::realpath(Platform::getCwd()))); - $vendorPath = $filesystem->normalizePath(Platform::realpath(Platform::realpath($config->get('vendor-dir')))); + $basePath = $filesystem->normalizePath(Platform::realpath(Platform::getCwd())); + $vendorPath = $filesystem->normalizePath(Platform::realpath($config->get('vendor-dir'))); $useGlobalIncludePath = $config->get('use-include-path'); $prependAutoloader = $config->get('prepend-autoloader') === false ? 'false' : 'true'; $targetDir = $vendorPath.'/'.$targetDir; diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php index 85ab6d9d9301..3d283b63c7b2 100644 --- a/src/Composer/Util/Platform.php +++ b/src/Composer/Util/Platform.php @@ -56,6 +56,9 @@ public static function getCwd(bool $allowEmpty = false): string /** * Infallible realpath version that falls back on the given $path if realpath is not working + * + * NB: Do not remove recursive `realpath()` calls. Fixes failing Windows `realpath()` implementation. + * @see https://bugs.php.net/bug.php?id=72738 */ public static function realpath(string $path): string { @@ -63,6 +66,9 @@ public static function realpath(string $path): string if ($realPath === false) { return $path; } + if ($realPath !== $path) { + return Platform::realpath($realPath); + } return $realPath; } From d1081a35f05fbf88b498a14f9bafd259ae5295c3 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Mon, 12 May 2025 23:51:44 -0700 Subject: [PATCH 08/40] Replace `realpath()` with `Platform::realpath()` --- src/Composer/Command/ConfigCommand.php | 2 +- src/Composer/Command/ShowCommand.php | 7 ++++--- src/Composer/Compiler.php | 3 ++- src/Composer/Console/Application.php | 4 ++-- src/Composer/Downloader/ArchiveDownloader.php | 4 ++-- src/Composer/Downloader/FileDownloader.php | 2 +- src/Composer/Downloader/GitDownloader.php | 2 +- src/Composer/Downloader/HgDownloader.php | 7 ++++--- src/Composer/Downloader/PathDownloader.php | 10 +++++----- src/Composer/Downloader/VcsDownloader.php | 3 ++- src/Composer/EventDispatcher/EventDispatcher.php | 2 +- src/Composer/Installer/BinaryInstaller.php | 6 +++--- src/Composer/Installer/LibraryInstaller.php | 2 +- src/Composer/Json/JsonFile.php | 3 ++- src/Composer/Package/Archiver/ArchiveManager.php | 3 ++- src/Composer/Package/Archiver/PharArchiver.php | 4 +++- src/Composer/Repository/FilesystemRepository.php | 4 ++-- src/Composer/Repository/PathRepository.php | 2 +- src/Composer/Repository/Vcs/GitDriver.php | 3 ++- 19 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index de3bd367ef77..018541d84aa2 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -189,7 +189,7 @@ protected function initialize(InputInterface $input, OutputInterface $output): v if ( ($configFile === 'composer.json' || $configFile === './composer.json') && !file_exists($configFile) - && realpath(Platform::getCwd()) === realpath($this->config->get('home')) + && Platform::realpath(Platform::getCwd()) === Platform::realpath($this->config->get('home')) ) { file_put_contents($configFile, "{\n}\n"); } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 14c9a4c32165..5b082afb85d3 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -43,6 +43,7 @@ use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; use Composer\Util\PackageInfo; +use Composer\Util\Platform; use DateTimeInterface; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Formatter\OutputFormatter; @@ -359,7 +360,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->write($package->getName(), false); $path = $composer->getInstallationManager()->getInstallPath($package); if (is_string($path)) { - $io->write(' ' . strtok(realpath($path), "\r\n")); + $io->write(' ' . strtok(Platform::realpath($path), "\r\n")); } else { $io->write(' null'); } @@ -581,7 +582,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($writePath) { $path = $composer->getInstallationManager()->getInstallPath($package); if (is_string($path)) { - $packageViewData['path'] = strtok(realpath($path), "\r\n"); + $packageViewData['path'] = strtok(Platform::realpath($path), "\r\n"); } else { $packageViewData['path'] = null; } @@ -903,7 +904,7 @@ protected function printMeta(CompletePackageInterface $package, array $versions, if ($isInstalledPackage) { $path = $this->requireComposer()->getInstallationManager()->getInstallPath($package); if (is_string($path)) { - $io->write('path : ' . realpath($path)); + $io->write('path : ' . Platform::realpath($path)); } else { $io->write('path : null'); } diff --git a/src/Composer/Compiler.php b/src/Composer/Compiler.php index ed5d3ad047ab..aff7bb7a36dc 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -15,6 +15,7 @@ use Composer\Json\JsonFile; use Composer\CaBundle\CaBundle; use Composer\Pcre\Preg; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Symfony\Component\Finder\Finder; use Symfony\Component\Process\Process; @@ -142,7 +143,7 @@ public function compile(string $pharFile = 'composer.phar'): void __DIR__ . '/../../vendor/symfony/console/Resources/bin/hiddeninput.exe', __DIR__ . '/../../vendor/symfony/console/Resources/completion.bash', ] as $file) { - $extraFiles[$file] = realpath($file); + $extraFiles[$file] = Platform::realpath($file); if (!file_exists($file)) { throw new \RuntimeException('Extra file listed is missing from the filesystem: '.$file); } diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 7fc75ff4c778..1faa0710d908 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -199,7 +199,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int && $input->hasParameterOption('-h', true) === false ) { $dir = dirname(Platform::getCwd(true)); - $home = realpath(Platform::getEnv('HOME') ?: Platform::getEnv('USERPROFILE') ?: '/'); + $home = Platform::realpath(Platform::getEnv('HOME') ?: Platform::getEnv('USERPROFILE') ?: '/'); // abort when we reach the home dir or top of the filesystem while (dirname($dir) !== $dir && $dir !== $home) { @@ -288,7 +288,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int } catch (ParsingException $e) { $details = $e->getDetails(); - $file = realpath(Factory::getComposerFile()); + $file = Platform::realpath(Factory::getComposerFile()); $line = null; if ($details && isset($details['line'])) { diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index ff132e28b419..c6d197696a4d 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -74,7 +74,7 @@ public function install(PackageInterface $package, string $path, bool $output = $this->addCleanupPath($package, $temporaryDir); // avoid cleaning up $path if installing in "." for eg create-project as we can not // delete the directory we are currently in on windows - if (!is_dir($path) || realpath($path) !== Platform::getCwd()) { + if (!is_dir($path) || Platform::realpath($path) !== Platform::getCwd()) { $this->addCleanupPath($package, $path); } @@ -89,7 +89,7 @@ public function install(PackageInterface $package, string $path, bool $output = // clean up $filesystem->removeDirectory($temporaryDir); - if (is_dir($path) && realpath($path) !== Platform::getCwd()) { + if (is_dir($path) && Platform::realpath($path) !== Platform::getCwd()) { $filesystem->removeDirectory($path); } $this->removeCleanupPath($package, $temporaryDir); diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 5f3b24279612..ccd63e355849 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -333,7 +333,7 @@ public function cleanup(string $type, PackageInterface $package, string $path, ? } foreach ($dirsToCleanUp as $dir) { - if (is_dir($dir) && $this->filesystem->isDirEmpty($dir) && realpath($dir) !== Platform::getCwd()) { + if (is_dir($dir) && $this->filesystem->isDirEmpty($dir) && Platform::realpath($dir) !== Platform::getCwd()) { $this->filesystem->removeDirectoryPhp($dir); } } diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index e54d95473f66..ddf85dd1f5a4 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -607,7 +607,7 @@ protected function normalizePath(string $path): string return $path; } - $path = rtrim(realpath($basePath) . '/' . implode('/', $removed), '/'); + $path = rtrim(Platform::realpath($basePath) . '/' . implode('/', $removed), '/'); } return $path; diff --git a/src/Composer/Downloader/HgDownloader.php b/src/Composer/Downloader/HgDownloader.php index 4709ae35ef38..eeb63e788ce4 100644 --- a/src/Composer/Downloader/HgDownloader.php +++ b/src/Composer/Downloader/HgDownloader.php @@ -12,6 +12,7 @@ namespace Composer\Downloader; +use Composer\Util\Platform; use React\Promise\PromiseInterface; use Composer\Package\PackageInterface; use Composer\Util\ProcessExecutor; @@ -48,7 +49,7 @@ protected function doInstall(PackageInterface $package, string $path, string $ur $hgUtils->runCommand($cloneCommand, $url, $path); $command = ['hg', 'up', '--', (string) $package->getSourceReference()]; - if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { + if (0 !== $this->process->execute($command, $ignoredOutput, Platform::realpath($path))) { throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); } @@ -91,7 +92,7 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin return null; } - $this->process->execute(['hg', 'st'], $output, realpath($path)); + $this->process->execute(['hg', 'st'], $output, Platform::realpath($path)); $output = trim($output); @@ -105,7 +106,7 @@ protected function getCommitLogs(string $fromReference, string $toReference, str { $command = ['hg', 'log', '-r', $fromReference.':'.$toReference, '--style', 'compact']; - if (0 !== $this->process->execute($command, $output, realpath($path))) { + if (0 !== $this->process->execute($command, $output, Platform::realpath($path))) { throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); } diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index f71ea2568a1a..dbf5904ccd99 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -55,11 +55,11 @@ public function download(PackageInterface $package, string $path, ?PackageInterf )); } - if (realpath($path) === $realUrl) { + if (Platform::realpath($path) === $realUrl) { return \React\Promise\resolve(null); } - if (strpos(realpath($path) . DIRECTORY_SEPARATOR, $realUrl . DIRECTORY_SEPARATOR) === 0) { + if (strpos(Platform::realpath($path) . DIRECTORY_SEPARATOR, $realUrl . DIRECTORY_SEPARATOR) === 0) { // IMPORTANT NOTICE: If you wish to change this, don't. You are wasting your time and ours. // // Please see https://github.com/composer/composer/pull/5974 and https://github.com/composer/composer/pull/6174 @@ -67,7 +67,7 @@ public function download(PackageInterface $package, string $path, ?PackageInterf throw new \RuntimeException(sprintf( 'Package %s cannot install to "%s" inside its source at "%s"', $package->getName(), - realpath($path), + Platform::realpath($path), $realUrl )); } @@ -90,7 +90,7 @@ public function install(PackageInterface $package, string $path, bool $output = throw new \RuntimeException('Failed to realpath '.$url); } - if (realpath($path) === $realUrl) { + if (Platform::realpath($path) === $realUrl) { if ($output) { $this->io->writeError(" - " . InstallOperation::format($package) . $this->getInstallOperationAppendix($package, $path)); } @@ -250,7 +250,7 @@ protected function getInstallOperationAppendix(PackageInterface $package, string throw new \RuntimeException('Failed to realpath '.$url); } - if (realpath($path) === $realUrl) { + if (Platform::realpath($path) === $realUrl) { return ': Source already present'; } diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index 626bcb5c5594..0ff8b3b26a75 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -17,6 +17,7 @@ use Composer\Package\PackageInterface; use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionParser; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use Composer\Util\Filesystem; @@ -350,7 +351,7 @@ private function prepareUrls(array $urls): array $url = rawurldecode($url); } - $urls[$index] = realpath($url); + $urls[$index] = Platform::realpath($url); if ($isFileProtocol) { $urls[$index] = $fileProtocol . $urls[$index]; diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 45cf3f1ddc82..ebc0a5400087 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -683,7 +683,7 @@ private function ensureBinDirIsInPath(): void // add the bin dir to the PATH to make local binaries of deps usable in scripts $binDir = $this->composer->getConfig()->get('bin-dir'); if (is_dir($binDir)) { - $binDir = realpath($binDir); + $binDir = Platform::realpath($binDir); $pathValue = (string) Platform::getEnv($pathEnv); if (!Preg::isMatch('{(^|'.PATH_SEPARATOR.')'.preg_quote($binDir).'($|'.PATH_SEPARATOR.')}', $pathValue)) { Platform::putEnv($pathEnv, $binDir.PATH_SEPARATOR.$pathValue); diff --git a/src/Composer/Installer/BinaryInstaller.php b/src/Composer/Installer/BinaryInstaller.php index 921132552792..a5f524335d86 100644 --- a/src/Composer/Installer/BinaryInstaller.php +++ b/src/Composer/Installer/BinaryInstaller.php @@ -76,7 +76,7 @@ public function installBinaries(PackageInterface $package, string $installPath, // $package, we can now safely turn it into a absolute path (as we // already checked the binary's existence). The following helpers // will require absolute paths to work properly. - $binPath = realpath($binPath); + $binPath = Platform::realpath($binPath); } $this->initializeBinDir(); $link = $this->binDir.'/'.basename($bin); @@ -87,7 +87,7 @@ public function installBinaries(PackageInterface $package, string $installPath, } continue; } - if (realpath($link) === realpath($binPath)) { + if (Platform::realpath($link) === Platform::realpath($binPath)) { // It is a linked binary from a previous installation, which can be replaced with a proxy file $this->filesystem->unlink($link); } @@ -180,7 +180,7 @@ protected function installUnixyProxyBinaries(string $binPath, string $link): voi protected function initializeBinDir(): void { $this->filesystem->ensureDirectoryExists($this->binDir); - $this->binDir = realpath($this->binDir); + $this->binDir = Platform::realpath($this->binDir); } protected function generateWindowsProxyCode(string $bin, string $link): string diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 0626fb189cde..b4ef1630332d 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -333,7 +333,7 @@ protected function removeCode(PackageInterface $package) protected function initializeVendorDir() { $this->filesystem->ensureDirectoryExists($this->vendorDir); - $this->vendorDir = realpath($this->vendorDir); + $this->vendorDir = Platform::realpath($this->vendorDir); } protected function getDownloadManager(): DownloadManager diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 785e46bfa2f0..bc0c7cdc6639 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -14,6 +14,7 @@ use Composer\Pcre\Preg; use Composer\Util\Filesystem; +use Composer\Util\Platform; use JsonSchema\Validator; use Seld\JsonLint\JsonParser; use Seld\JsonLint\ParsingException; @@ -148,7 +149,7 @@ public function write(array $hash, int $options = JSON_UNESCAPED_SLASHES | JSON_ if (!is_dir($dir)) { if (file_exists($dir)) { throw new \UnexpectedValueException( - realpath($dir).' exists and is not a directory.' + Platform::realpath($dir).' exists and is not a directory.' ); } if (!@mkdir($dir, 0777, true)) { diff --git a/src/Composer/Package/Archiver/ArchiveManager.php b/src/Composer/Package/Archiver/ArchiveManager.php index 77c3ebe3dcc1..6d90c66c622c 100644 --- a/src/Composer/Package/Archiver/ArchiveManager.php +++ b/src/Composer/Package/Archiver/ArchiveManager.php @@ -17,6 +17,7 @@ use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Loop; +use Composer\Util\Platform; use Composer\Util\SyncHelper; use Composer\Json\JsonFile; use Composer\Package\CompletePackageInterface; @@ -208,7 +209,7 @@ public function archive(CompletePackageInterface $package, string $format, strin // Archive filename $filesystem->ensureDirectoryExists($targetDir); - $target = realpath($targetDir).'/'.$packageName.'.'.$format; + $target = Platform::realpath($targetDir).'/'.$packageName.'.'.$format; $filesystem->ensureDirectoryExists(dirname($target)); if (!$this->overwriteFiles && file_exists($target)) { diff --git a/src/Composer/Package/Archiver/PharArchiver.php b/src/Composer/Package/Archiver/PharArchiver.php index c3d025f8d8af..8fc33be181c2 100644 --- a/src/Composer/Package/Archiver/PharArchiver.php +++ b/src/Composer/Package/Archiver/PharArchiver.php @@ -12,6 +12,8 @@ namespace Composer\Package\Archiver; +use Composer\Util\Platform; + /** * @author Till Klampaeckel * @author Nils Adermann @@ -38,7 +40,7 @@ class PharArchiver implements ArchiverInterface */ public function archive(string $sources, string $target, string $format, array $excludes = [], bool $ignoreFilters = false): string { - $sources = realpath($sources); + $sources = Platform::realpath($sources); // Phar would otherwise load the file which we don't want if (file_exists($target)) { diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index 6ae6b149f0fc..e1c726c2c6cc 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -129,7 +129,7 @@ public function write(bool $devMode, InstallationManager $installationManager) $repoDir = dirname($this->file->getPath()); $this->filesystem->ensureDirectoryExists($repoDir); - $repoDir = $this->filesystem->normalizePath(realpath($repoDir)); + $repoDir = $this->filesystem->normalizePath(Platform::realpath($repoDir)); $installPaths = []; foreach ($this->getCanonicalPackages() as $package) { @@ -375,7 +375,7 @@ private function dumpInstalledPackage(PackageInterface $package, array $installP } if ($package instanceof RootPackageInterface) { - $to = $this->filesystem->normalizePath(realpath(Platform::getCwd())); + $to = $this->filesystem->normalizePath(Platform::realpath(Platform::getCwd())); $installPath = $this->filesystem->findShortestPath($repoDir, $to, true); } else { $installPath = $installPaths[$package->getName()]; diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 0b8d992360fb..ba995b2e0023 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -164,7 +164,7 @@ protected function initialize(): void } foreach ($urlMatches as $url) { - $path = realpath($url) . DIRECTORY_SEPARATOR; + $path = Platform::realpath($url) . DIRECTORY_SEPARATOR; $composerFilePath = $path.'composer.json'; if (!file_exists($composerFilePath)) { diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index 7be1faba058d..9c554cda7958 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -13,6 +13,7 @@ namespace Composer\Repository\Vcs; use Composer\Pcre\Preg; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; use Composer\Util\Url; @@ -46,7 +47,7 @@ public function initialize(): void throw new \RuntimeException('Failed to read package information from '.$this->url.' as the path does not exist'); } $this->repoDir = $this->url; - $cacheUrl = realpath($this->url); + $cacheUrl = Platform::realpath($this->url); } else { if (!Cache::isUsable($this->config->get('cache-vcs-dir'))) { throw new \RuntimeException('GitDriver requires a usable cache directory, and it looks like you set it to be disabled'); From 4b5129b8391eb20f666b5a6bd266c0ec4382146a Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Tue, 13 May 2025 07:01:46 -0700 Subject: [PATCH 09/40] Update src/Composer/Util/Filesystem.php Co-authored-by: Jordi Boggiano --- src/Composer/Util/Filesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 83302cb9bed3..e0ece2ce47da 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -570,7 +570,7 @@ public function findShortestPathCode(string $from, string $to, bool $directories */ public function isAbsolutePath(string $path) { - $path = str_replace('file://', '', $path); + $path = Preg::replace('{^file://}i', '', $path); return strpos($path, '/') === 0 || substr($path, 1, 1) === ':' From 21b7a7eb2f8d0bf9fb93900e98396b1ebf124e61 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 5 Jun 2025 21:42:44 -0700 Subject: [PATCH 10/40] Refactor `::isStreamWrapperPath()` to `public static` method --- src/Composer/Util/Filesystem.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index e0ece2ce47da..3ef5cc1f1884 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -30,14 +30,13 @@ class Filesystem private $processExecutor; /** - * @var non-empty-string + * @var ?non-empty-string */ - private $streamWrappersRegex; + private static $streamWrappersRegex = null; public function __construct(?ProcessExecutor $executor = null) { $this->processExecutor = $executor; - $this->streamWrappersRegex = sprintf('{^(?:%s)://}', implode('|', array_map('preg_quote', stream_get_wrappers()))); } /** @@ -575,7 +574,7 @@ public function isAbsolutePath(string $path) return strpos($path, '/') === 0 || substr($path, 1, 1) === ':' || strpos($path, '\\\\') === 0 - || Preg::isMatch($this->streamWrappersRegex, $path); + || self::isStreamWrapperPath($path); } /** @@ -664,6 +663,15 @@ public static function trimTrailingSlash(string $path) return $path; } + public static function isStreamWrapperPath(string $path): bool + { + if(!isset(self::$streamWrappersRegex)) { + self::$streamWrappersRegex = sprintf( '{^(?:%s)://}', implode( '|', array_map( 'preg_quote', stream_get_wrappers() ) ) ); + } + + return Preg::isMatch(self::$streamWrappersRegex, $path); + } + /** * Return if the given path is local * From a23e7cefdbce25ff37b4b257db24032320916fd9 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 5 Jun 2025 22:02:50 -0700 Subject: [PATCH 11/40] Do not treat `file://` as a streamwrapper --- src/Composer/Util/Filesystem.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 3ef5cc1f1884..b7dc666202e0 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -669,6 +669,8 @@ public static function isStreamWrapperPath(string $path): bool self::$streamWrappersRegex = sprintf( '{^(?:%s)://}', implode( '|', array_map( 'preg_quote', stream_get_wrappers() ) ) ); } + $path = Preg::replace('{^file://}i', '', $path); + return Preg::isMatch(self::$streamWrappersRegex, $path); } From 40eb099a34a4ce6c7477683d6571b760f351ef2a Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 5 Jun 2025 22:04:38 -0700 Subject: [PATCH 12/40] Return existing path for streamwrapper; throw exception for invalid path --- src/Composer/Util/Platform.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php index 3d283b63c7b2..e8353d19b857 100644 --- a/src/Composer/Util/Platform.php +++ b/src/Composer/Util/Platform.php @@ -59,12 +59,18 @@ public static function getCwd(bool $allowEmpty = false): string * * NB: Do not remove recursive `realpath()` calls. Fixes failing Windows `realpath()` implementation. * @see https://bugs.php.net/bug.php?id=72738 + * + * @throws \RuntimeException If the path does not exist. Distinct from `\realpath()` returning `false` in that case. */ public static function realpath(string $path): string { + if(Filesystem::isStreamWrapperPath($path)) { + return $path; + } + $realPath = realpath($path); if ($realPath === false) { - return $path; + throw new \RuntimeException('Path does not exist: ' . $path); } if ($realPath !== $path) { return Platform::realpath($realPath); From 0cd95aa44e84b64a0af61e52f8d8ff76b846c90f Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 5 Jun 2025 22:24:49 -0700 Subject: [PATCH 13/40] Move `stream_wrapper_register()` to `bootstrap.php` --- tests/Composer/Test/Util/FilesystemTest.php | 6 +----- tests/bootstrap.php | 9 +++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index 6ae1e4025818..ad5a1f8370ec 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -412,7 +412,7 @@ public static function isAbsolutePathDataProvider(): array 'unixPath' => ['/foo/bar', true], 'smbPath' => ['\\\\smb\\folder\\file.txt', true], 'windowsPath' => ['C:\\foo\\bar', true], - 'streamPath' => ['customstreamwrapper://path/to/whatever', true], + 'streamPath' => ['composertestsstreamwrapper://path/to/whatever', true], 'fileStreamAbsolutePath' => ['file:///path/to/whatever', true], 'fileStreamRelativePath' => ['file://path/to/whatever', false], 'relativeSubPath' => ['foo/bar', false], @@ -427,10 +427,6 @@ public static function isAbsolutePathDataProvider(): array */ public function testIsAbsolutePath(string $path, bool $expected): void { - if (str_starts_with($path, 'customstreamwrapper://') && !in_array('customstreamwrapper', stream_get_wrappers(), true)) { - stream_wrapper_register('customstreamwrapper', __CLASS__); - } - self::assertEquals($expected, (new Filesystem())->isAbsolutePath($path)); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2213d74214c4..2ddd37bc06bc 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -35,3 +35,12 @@ Platform::clearEnv('COMPOSER'); Platform::clearEnv('COMPOSER_VENDOR_DIR'); Platform::clearEnv('COMPOSER_BIN_DIR'); + +/** + * The stream wrapper checks use a static property to store the stream wrapper names. If it is accessed before + * the stream wrapper is registered, and a later test relies on that stream wrapper, the static property will be + * outdated and the test will fail. + * + * @see \Composer\Util\Filesystem::isStreamWrapperPath() + */ +stream_wrapper_register('composertestsstreamwrapper', stdclass::class); From 598e88e43cb5f9d6d2bf444b911cee532fa6d428 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Wed, 11 Jun 2025 21:39:57 -0700 Subject: [PATCH 14/40] Replace `realpath()` with `Platform::realpath()` --- src/Composer/Autoload/AutoloadGenerator.php | 6 +++++- src/Composer/Command/ClearCacheCommand.php | 6 ++++-- src/Composer/Command/CreateProjectCommand.php | 4 +++- src/Composer/Command/InitCommand.php | 12 ++++++------ src/Composer/Command/ShowCommand.php | 12 +++++++----- src/Composer/Downloader/ArchiveDownloader.php | 6 ++---- src/Composer/Downloader/PathDownloader.php | 19 +++++++++---------- src/Composer/Installer/BinaryInstaller.php | 5 +---- src/Composer/Json/JsonFile.php | 5 +++-- .../Archiver/ArchivableFilesFinder.php | 6 ++++-- src/Composer/Package/Archiver/ZipArchiver.php | 9 +++++---- src/Composer/Package/Locker.php | 9 +++++++-- 12 files changed, 56 insertions(+), 43 deletions(-) diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 46e0f2df63ee..13a743979c56 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -1288,7 +1288,11 @@ static function ($matches) use (&$updir): string { $installPath = strtr(Platform::getCwd(), '\\', '/'); } - $resolvedPath = Platform::realpath($installPath . '/' . $updir); + try { + $resolvedPath = Platform::realpath($installPath . '/' . $updir); + } catch (\RuntimeException $e) { + continue; + } $autoloads[] = preg_quote(strtr($resolvedPath, '\\', '/')) . '/' . $path . '($|/)'; continue; diff --git a/src/Composer/Command/ClearCacheCommand.php b/src/Composer/Command/ClearCacheCommand.php index 77ed517a801c..c9b19d6dda8a 100644 --- a/src/Composer/Command/ClearCacheCommand.php +++ b/src/Composer/Command/ClearCacheCommand.php @@ -14,6 +14,7 @@ use Composer\Cache; use Composer\Factory; +use Composer\Util\Platform; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -67,8 +68,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } - $cachePath = realpath($cachePath); - if (!$cachePath) { + try { + $cachePath = Platform::realpath($cachePath); + } catch (\RuntimeException $e) { $io->writeError("Cache directory does not exist ($key): $cachePath"); continue; diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 368516fdb55a..2e74bda0fc2a 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -432,13 +432,15 @@ protected function installRootPackage(InputInterface $input, IOInterface $io, Co // handler Ctrl+C aborts gracefully @mkdir($directory, 0777, true); - if (false !== ($realDir = realpath($directory))) { + try { + $realDir = Platform::realpath($directory); $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) use ($realDir) { $this->getIO()->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG); $fs = new Filesystem(); $fs->removeDirectory($realDir); $handler->exitWithLastSignal(); }); + } catch (\RuntimeException $exception) { } // avoid displaying 9999999-dev as version if default-branch was selected diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 9a49d595bb97..6d8d579d5004 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -23,6 +23,7 @@ use Composer\Repository\RepositoryFactory; use Composer\Spdx\SpdxLicenses; use Composer\Util\Filesystem; +use Composer\Util\Platform; use Composer\Util\Silencer; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; @@ -181,10 +182,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($input->isInteractive() && is_dir('.git')) { - $ignoreFile = realpath('.gitignore'); - - if (false === $ignoreFile) { - $ignoreFile = realpath('.') . '/.gitignore'; + try { + $ignoreFile = Platform::realpath('.gitignore'); + } catch (\RuntimeException $exception) { + $ignoreFile = Platform::realpath('.') . '/.gitignore'; } if (!$this->hasVendorIgnore($ignoreFile)) { @@ -269,10 +270,9 @@ protected function interact(InputInterface $input, OutputInterface $output) '', ]); - $cwd = realpath("."); - $name = $input->getOption('name'); if (null === $name) { + $cwd = Platform::realpath("."); $name = basename($cwd); $name = $this->sanitizePackageNameComponent($name); diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 5b082afb85d3..20bedab24a00 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -904,7 +904,11 @@ protected function printMeta(CompletePackageInterface $package, array $versions, if ($isInstalledPackage) { $path = $this->requireComposer()->getInstallationManager()->getInstallPath($package); if (is_string($path)) { - $io->write('path : ' . Platform::realpath($path)); + try { + $io->write('path : ' . Platform::realpath($path)); + } catch (\RuntimeException $exception) { + $io->write('path : '); + } } else { $io->write('path : null'); } @@ -1065,10 +1069,8 @@ protected function printPackageInfoAsJson(CompletePackageInterface $package, arr if (!PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package)) { $path = $this->requireComposer()->getInstallationManager()->getInstallPath($package); if (is_string($path)) { - $path = realpath($path); - if ($path !== false) { - $json['path'] = $path; - } + $path = Platform::realpath($path); + $json['path'] = $path; } else { $json['path'] = null; } diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index c6d197696a4d..b6df2e9d4ed6 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -93,10 +93,8 @@ public function install(PackageInterface $package, string $path, bool $output = $filesystem->removeDirectory($path); } $this->removeCleanupPath($package, $temporaryDir); - $realpath = realpath($path); - if ($realpath !== false) { - $this->removeCleanupPath($package, $realpath); - } + $realpath = Platform::realpath($path); + $this->removeCleanupPath($package, $realpath); }; try { diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index dbf5904ccd99..396647453760 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -46,8 +46,9 @@ public function download(PackageInterface $package, string $path, ?PackageInterf if (null === $url) { throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot download.'); } - $realUrl = realpath($url); - if (false === $realUrl || !file_exists($realUrl) || !is_dir($realUrl)) { + try { + $realUrl = Platform::realpath($url); + } catch (\RuntimeException $exception) { throw new \RuntimeException(sprintf( 'Source path "%s" is not found for package %s', $url, @@ -55,11 +56,12 @@ public function download(PackageInterface $package, string $path, ?PackageInterf )); } - if (Platform::realpath($path) === $realUrl) { + $realPath = Platform::realpath($path); + if ($realPath === $realUrl) { return \React\Promise\resolve(null); } - if (strpos(Platform::realpath($path) . DIRECTORY_SEPARATOR, $realUrl . DIRECTORY_SEPARATOR) === 0) { + if (strpos($realPath . DIRECTORY_SEPARATOR, $realUrl . DIRECTORY_SEPARATOR) === 0) { // IMPORTANT NOTICE: If you wish to change this, don't. You are wasting your time and ours. // // Please see https://github.com/composer/composer/pull/5974 and https://github.com/composer/composer/pull/6174 @@ -67,7 +69,7 @@ public function download(PackageInterface $package, string $path, ?PackageInterf throw new \RuntimeException(sprintf( 'Package %s cannot install to "%s" inside its source at "%s"', $package->getName(), - Platform::realpath($path), + $realPath, $realUrl )); } @@ -85,7 +87,7 @@ public function install(PackageInterface $package, string $path, bool $output = if (null === $url) { throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot install.'); } - $realUrl = realpath($url); + $realUrl = Platform::realpath($url); if (false === $realUrl) { throw new \RuntimeException('Failed to realpath '.$url); } @@ -245,10 +247,7 @@ protected function getInstallOperationAppendix(PackageInterface $package, string if (null === $url) { throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot install.'); } - $realUrl = realpath($url); - if (false === $realUrl) { - throw new \RuntimeException('Failed to realpath '.$url); - } + $realUrl = Platform::realpath($url); if (Platform::realpath($path) === $realUrl) { return ': Source already present'; diff --git a/src/Composer/Installer/BinaryInstaller.php b/src/Composer/Installer/BinaryInstaller.php index a5f524335d86..02833112fffd 100644 --- a/src/Composer/Installer/BinaryInstaller.php +++ b/src/Composer/Installer/BinaryInstaller.php @@ -226,10 +226,7 @@ protected function generateUnixyProxyCode(string $bin, string $link): string // Don't expose autoload path when vendor dir was not set in custom installers if ($this->vendorDir !== null) { // ensure comparisons work accurately if the CWD is a symlink, as $link is realpath'd already - $vendorDirReal = realpath($this->vendorDir); - if ($vendorDirReal === false) { - $vendorDirReal = $this->vendorDir; - } + $vendorDirReal = Platform::realpath($this->vendorDir); $globalsCode .= '$GLOBALS[\'_composer_autoload_path\'] = ' . $this->filesystem->findShortestPathCode($link, $vendorDirReal . '/autoload.php', false, true).";\n"; } // Add workaround for PHPUnit process isolation diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index bc0c7cdc6639..68eba909f8f9 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -106,10 +106,11 @@ public function read() } if ($this->io && $this->io->isDebug()) { $realpathInfo = ''; - $realpath = realpath($this->path); - if (false !== $realpath && $realpath !== $this->path) { + $realpath = Platform::realpath($this->path); + if ($realpath !== $this->path) { $realpathInfo = ' (' . $realpath . ')'; } + $this->io->writeError('Reading ' . $this->path . $realpathInfo); } $json = file_get_contents($this->path); diff --git a/src/Composer/Package/Archiver/ArchivableFilesFinder.php b/src/Composer/Package/Archiver/ArchivableFilesFinder.php index 2cf7ffc72e0a..40df8424a128 100644 --- a/src/Composer/Package/Archiver/ArchivableFilesFinder.php +++ b/src/Composer/Package/Archiver/ArchivableFilesFinder.php @@ -14,6 +14,7 @@ use Composer\Pcre\Preg; use Composer\Util\Filesystem; +use Composer\Util\Platform; use FilesystemIterator; use FilterIterator; use Iterator; @@ -47,8 +48,9 @@ public function __construct(string $sources, array $excludes, bool $ignoreFilter { $fs = new Filesystem(); - $sourcesRealPath = realpath($sources); - if ($sourcesRealPath === false) { + try { + $sourcesRealPath = Platform::realpath($sources); + } catch (\RuntimeException $exception) { throw new \RuntimeException('Could not realpath() the source directory "'.$sources.'"'); } $sources = $fs->normalizePath($sourcesRealPath); diff --git a/src/Composer/Package/Archiver/ZipArchiver.php b/src/Composer/Package/Archiver/ZipArchiver.php index 54ca20a41592..430733206feb 100644 --- a/src/Composer/Package/Archiver/ZipArchiver.php +++ b/src/Composer/Package/Archiver/ZipArchiver.php @@ -32,11 +32,12 @@ class ZipArchiver implements ArchiverInterface public function archive(string $sources, string $target, string $format, array $excludes = [], bool $ignoreFilters = false): string { $fs = new Filesystem(); - $sourcesRealpath = realpath($sources); - if (false !== $sourcesRealpath) { - $sources = $sourcesRealpath; + + try { + $sources = Platform::realpath($sources); + } catch (\Exception $exception) { } - unset($sourcesRealpath); + $sources = $fs->normalizePath($sources); $zip = new ZipArchive(); diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 38cd8ef3a92d..812d6a907ce4 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -19,6 +19,7 @@ use Composer\Repository\LockArrayRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RootPackageRepository; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Loader\ArrayLoader; @@ -544,11 +545,15 @@ private function getPackageTime(PackageInterface $package): ?string if ($path === null) { return null; } - $path = realpath($path); + try { + $path = Platform::realpath($path); + } catch (\RuntimeException $exception) { + return null; + } $sourceType = $package->getSourceType(); $datetime = null; - if ($path && in_array($sourceType, ['git', 'hg'])) { + if (in_array($sourceType, ['git', 'hg'])) { $sourceRef = $package->getSourceReference() ?: $package->getDistReference(); switch ($sourceType) { case 'git': From 413dd88e54d45b16be5d574fa0c6fab45337f7ee Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 13:39:47 -0700 Subject: [PATCH 15/40] Add test `::realpath()` throws `RuntimeException` --- tests/Composer/Test/Util/PlatformTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Composer/Test/Util/PlatformTest.php b/tests/Composer/Test/Util/PlatformTest.php index bbaa4ec82a9e..60c1e2c63350 100644 --- a/tests/Composer/Test/Util/PlatformTest.php +++ b/tests/Composer/Test/Util/PlatformTest.php @@ -14,6 +14,7 @@ use Composer\Util\Platform; use Composer\Test\TestCase; +use RuntimeException; /** * PlatformTest @@ -36,4 +37,11 @@ public function testIsWindows(): void self::assertEquals(('\\' === DIRECTORY_SEPARATOR), Platform::isWindows()); self::assertEquals(defined('PHP_WINDOWS_VERSION_MAJOR'), Platform::isWindows()); } + + public function testRealPathException(): void + { + // Test that `::realpath()` throws an exception on a non-existing path + $this->expectException(RuntimeException::class); + Platform::realpath('/path/does/not/exist'); + } } From d859488a940921ab30996cd49b904fed4bc58203 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 14:23:06 -0700 Subject: [PATCH 16/40] Add `::testIsStreamWrapperPath()` --- src/Composer/Util/Filesystem.php | 11 ++++++++- tests/Composer/Test/Util/FilesystemTest.php | 27 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index b7dc666202e0..acb9baef5371 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -663,10 +663,19 @@ public static function trimTrailingSlash(string $path) return $path; } + /** + * Is the file addressed with a streamwrapper:// prefix? + * + * `file://` is excluded – its paths can be relative, distinct from other stream wrappers which are always absolute. + * + * @see https://www.php.net/manual/en/intro.stream.php + * + * @param string $path Path to check + */ public static function isStreamWrapperPath(string $path): bool { - if(!isset(self::$streamWrappersRegex)) { self::$streamWrappersRegex = sprintf( '{^(?:%s)://}', implode( '|', array_map( 'preg_quote', stream_get_wrappers() ) ) ); + self::$streamWrappersRegex = sprintf('{^(?:%s)://}', implode('|', array_map('preg_quote', stream_get_wrappers()))); } $path = Preg::replace('{^file://}i', '', $path); diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index ad5a1f8370ec..0025c2caa67b 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -429,4 +429,31 @@ public function testIsAbsolutePath(string $path, bool $expected): void { self::assertEquals($expected, (new Filesystem())->isAbsolutePath($path)); } + + /** + * Same dataset as {@see FilesystemTest::isAbsolutePathDataProvider} but with different expected results. + */ + public static function isStreamWrapperPathProvider(): array + { + return [ + 'unixPath' => ['/foo/bar', false], + 'smbPath' => ['\\\\smb\\folder\\file.txt', false], + 'windowsPath' => ['C:\\foo\\bar', false], + 'streamPath' => ['composertestsstreamwrapper://path/to/whatever', true], + 'fileStreamAbsolutePath' => ['file:///path/to/whatever', false], + 'fileStreamRelativePath' => ['file://path/to/whatever', false], + 'relativeSubPath' => ['foo/bar', false], + 'relativeSubPath2' => ['./foo/bar', false], + 'relativeParentPath' => ['../foo/bar', false], + ]; + } + + /** + * @covers \Composer\Util\Filesystem::isStreamWrapperPath + * @dataProvider isStreamWrapperPathProvider + */ + public function testIsStreamWrapperPath(string $path, bool $expected): void + { + self::assertEquals($expected, Filesystem::isStreamWrapperPath($path)); + } } From 7542d719840232718b8c91730e60a8f31b152b3e Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 14:32:09 -0700 Subject: [PATCH 17/40] Add `::testIsLocalPath()` --- tests/Composer/Test/Util/FilesystemTest.php | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index 0025c2caa67b..d96e674e7ea6 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -456,4 +456,36 @@ public function testIsStreamWrapperPath(string $path, bool $expected): void { self::assertEquals($expected, Filesystem::isStreamWrapperPath($path)); } + + /** + * Similar dataset as {@see FilesystemTest::isAbsolutePathDataProvider} but with different expected results. + */ + public static function isLocalPathProvider(): array + { + return [ + 'unixPath' => ['/foo/bar', true], + 'smbPath' => ['\\\\smb\\folder\\file.txt', false], + 'windowsPath' => ['C:\\foo\\bar', true], + 'streamPath' => ['composertestsstreamwrapper://path/to/whatever', false], + 'fileStreamAbsolutePath' => ['file:///path/to/whatever', true], + 'fileStreamRelativePath' => ['file://path/to/whatever', true], + 'relativeSubPath' => ['foo/bar', true], + 'relativeSubPath2' => ['./foo/bar', true], + 'relativeParentPath' => ['../foo/bar', true], + ]; + } + + /** + * The purpose of this method is to determine if a cache should be used. With stream wrappers, we can't tell + * if the path is local or not. Currently, we return false, meaning that a cache typically will be used. But + * it could be argued that caching is the business of whoever is implementing the stream wrapper, and we should + * treat it like a local path. + * + * @covers \Composer\Util\Filesystem::isLocalPath + * @dataProvider isLocalPathProvider + */ + public function testIsLocalPath(string $path, bool $expected): void + { + self::assertEquals($expected, Filesystem::isLocalPath($path)); + } } From faeac4255aa82bffcb6aab4e36c00eb16f90bfd3 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 20:35:47 -0700 Subject: [PATCH 18/40] Remove outdated redundant throw --- src/Composer/Downloader/PathDownloader.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index 396647453760..e456f98e0007 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -88,9 +88,6 @@ public function install(PackageInterface $package, string $path, bool $output = throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot install.'); } $realUrl = Platform::realpath($url); - if (false === $realUrl) { - throw new \RuntimeException('Failed to realpath '.$url); - } if (Platform::realpath($path) === $realUrl) { if ($output) { From aa70941a3a5ddc65ccfcfb947ab012acbdd85650 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 20:36:45 -0700 Subject: [PATCH 19/40] Return `false` on `::realpath()` `RuntimeException` --- src/Composer/Installer/LibraryInstaller.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index b4ef1630332d..6a0193609f77 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -23,6 +23,7 @@ use Composer\Util\Platform; use React\Promise\PromiseInterface; use Composer\Downloader\DownloadManager; +use RuntimeException; /** * Package installation manager. @@ -93,7 +94,9 @@ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface } if (is_link($installPath)) { - if (realpath($installPath) === false) { + try { + Platform::realpath($installPath); + } catch (RuntimeException $exception) { return false; } From 14309188a45e88d1249e0fd2fe4a0f021006c43b Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 20:38:28 -0700 Subject: [PATCH 20/40] `continue` on failed `::realpath()` --- src/Composer/Repository/PathRepository.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index ba995b2e0023..a45298adaaa3 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -164,12 +164,12 @@ protected function initialize(): void } foreach ($urlMatches as $url) { - $path = Platform::realpath($url) . DIRECTORY_SEPARATOR; - $composerFilePath = $path.'composer.json'; - - if (!file_exists($composerFilePath)) { + try { + $path = Platform::realpath($url) . DIRECTORY_SEPARATOR; + } catch (\RuntimeException $exception) { continue; } + $composerFilePath = $path.'composer.json'; $json = file_get_contents($composerFilePath); $package = JsonFile::parseJson($json, $composerFilePath); From 1baf8bda60c01f67c0fd2a91004e93ecc9a4cd43 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 20:39:34 -0700 Subject: [PATCH 21/40] Strip `file://` before running system `realpath()` --- src/Composer/Util/Platform.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php index e8353d19b857..6bde5901c8fa 100644 --- a/src/Composer/Util/Platform.php +++ b/src/Composer/Util/Platform.php @@ -64,7 +64,9 @@ public static function getCwd(bool $allowEmpty = false): string */ public static function realpath(string $path): string { - if(Filesystem::isStreamWrapperPath($path)) { + $path = Preg::replace('{^file://}i', '', $path); + + if (Filesystem::isStreamWrapperPath($path)) { return $path; } From 2c9ed1fcde62fbb28e4dc279bd68fafa4ea4c25d Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 20:40:02 -0700 Subject: [PATCH 22/40] Add `::testRealPathFileStreamStripsScheme()` --- tests/Composer/Test/Util/PlatformTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Composer/Test/Util/PlatformTest.php b/tests/Composer/Test/Util/PlatformTest.php index 60c1e2c63350..c92eeb999daf 100644 --- a/tests/Composer/Test/Util/PlatformTest.php +++ b/tests/Composer/Test/Util/PlatformTest.php @@ -38,6 +38,13 @@ public function testIsWindows(): void self::assertEquals(defined('PHP_WINDOWS_VERSION_MAJOR'), Platform::isWindows()); } + public function testRealPathFileStreamStripsScheme(): void + { + $file = __FILE__; + $streamPath = 'file://' . $file; + self::assertEquals($file, Platform::realpath($streamPath)); + } + public function testRealPathException(): void { // Test that `::realpath()` throws an exception on a non-existing path From d32e015b21283d41f00a17ff2a532df1905c609e Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 20:43:43 -0700 Subject: [PATCH 23/40] Update `::isStreamWrapperPath() I'm not really sure how this wasn't part of an earlier commit --- src/Composer/Util/Filesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index acb9baef5371..431cafeed9b1 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -674,7 +674,7 @@ public static function trimTrailingSlash(string $path) */ public static function isStreamWrapperPath(string $path): bool { - self::$streamWrappersRegex = sprintf( '{^(?:%s)://}', implode( '|', array_map( 'preg_quote', stream_get_wrappers() ) ) ); + if (!isset(self::$streamWrappersRegex)) { self::$streamWrappersRegex = sprintf('{^(?:%s)://}', implode('|', array_map('preg_quote', stream_get_wrappers()))); } From 463cebefe1f9a1f5da6bace41cd7e4da04019b7c Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 20:44:20 -0700 Subject: [PATCH 24/40] Don't `file://` prefix stream paths --- src/Composer/Repository/Vcs/SvnDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Repository/Vcs/SvnDriver.php b/src/Composer/Repository/Vcs/SvnDriver.php index 9a303ee14997..9ef652e2bf2d 100644 --- a/src/Composer/Repository/Vcs/SvnDriver.php +++ b/src/Composer/Repository/Vcs/SvnDriver.php @@ -361,7 +361,7 @@ public static function supports(IOInterface $io, Config $config, string $url, bo protected static function normalizeUrl(string $url): string { $fs = new Filesystem(); - if ($fs->isAbsolutePath($url)) { + if ($fs->isAbsolutePath($url) && !Filesystem::isStreamWrapperPath($url)) { return 'file://' . strtr($url, '\\', '/'); } From 32e6c08fb83eecce93f32f485f29647190397359 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 20:56:47 -0700 Subject: [PATCH 25/40] No need to catch and throw `RuntimeException` here, the message was fine --- src/Composer/Package/Archiver/ArchivableFilesFinder.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Composer/Package/Archiver/ArchivableFilesFinder.php b/src/Composer/Package/Archiver/ArchivableFilesFinder.php index 40df8424a128..4d0cfd5831e8 100644 --- a/src/Composer/Package/Archiver/ArchivableFilesFinder.php +++ b/src/Composer/Package/Archiver/ArchivableFilesFinder.php @@ -48,11 +48,7 @@ public function __construct(string $sources, array $excludes, bool $ignoreFilter { $fs = new Filesystem(); - try { - $sourcesRealPath = Platform::realpath($sources); - } catch (\RuntimeException $exception) { - throw new \RuntimeException('Could not realpath() the source directory "'.$sources.'"'); - } + $sourcesRealPath = Platform::realpath($sources); $sources = $fs->normalizePath($sourcesRealPath); if ($ignoreFilters) { From d359604c09f9dd0b807d76fbc8ed9b17248ae789 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 21:48:39 -0700 Subject: [PATCH 26/40] Preserve `null` or `realpath()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maybe `is_string()` isn't necessary – `\Throwable` could be caught instead --- src/Composer/Command/ShowCommand.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 20bedab24a00..23e170105d93 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -1068,11 +1068,12 @@ protected function printPackageInfoAsJson(CompletePackageInterface $package, arr if (!PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package)) { $path = $this->requireComposer()->getInstallationManager()->getInstallPath($package); + $json['path'] = null; if (is_string($path)) { - $path = Platform::realpath($path); - $json['path'] = $path; - } else { - $json['path'] = null; + try { + $json['path'] = Platform::realpath($path); + } catch (\RuntimeException $exception) { + } } if ($package->getReleaseDate() !== null) { From 6592d1d61ce546a654ae6ccfd2d19ae7689ffe7c Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 3 Jul 2025 21:57:19 -0700 Subject: [PATCH 27/40] Update PhpDoc arguing we how we treat stream wrappers in `::isLocalPath()` --- tests/Composer/Test/Util/FilesystemTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index d96e674e7ea6..2a3bb166d353 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -476,10 +476,10 @@ public static function isLocalPathProvider(): array } /** - * The purpose of this method is to determine if a cache should be used. With stream wrappers, we can't tell - * if the path is local or not. Currently, we return false, meaning that a cache typically will be used. But - * it could be argued that caching is the business of whoever is implementing the stream wrapper, and we should - * treat it like a local path. + * The purpose of this method is generally to determine if a cache should be used. With stream wrappers, we can't + * tell if the path is local or not. Currently, we return false, meaning that a cache typically will be used. While + * it could be argued that caching is the business of whoever is implementing the stream wrapper, some such as + * http:// and git:// are clearly remote paths, so we should return false for `::isLocalPath()` on stream wrappers. * * @covers \Composer\Util\Filesystem::isLocalPath * @dataProvider isLocalPathProvider From 95353bc6463eea59fadfdd3c6fd4fe3ddc76831a Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Fri, 4 Jul 2025 13:47:38 -0700 Subject: [PATCH 28/40] Preserver behaviour; redundant throw --- src/Composer/Compiler.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Composer/Compiler.php b/src/Composer/Compiler.php index aff7bb7a36dc..d3b732d19900 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -143,8 +143,9 @@ public function compile(string $pharFile = 'composer.phar'): void __DIR__ . '/../../vendor/symfony/console/Resources/bin/hiddeninput.exe', __DIR__ . '/../../vendor/symfony/console/Resources/completion.bash', ] as $file) { - $extraFiles[$file] = Platform::realpath($file); - if (!file_exists($file)) { + try { + $extraFiles[$file] = Platform::realpath($file); + } catch (\RuntimeException $e) { throw new \RuntimeException('Extra file listed is missing from the filesystem: '.$file); } } From ea4e18b50e399a9b17a0a051d878fce219814422 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Wed, 23 Jul 2025 22:18:58 -0700 Subject: [PATCH 29/40] Update `::configFromString()` to return pretty `url` in `$repoConfig` The `$json->read()` line above would have thrown a `RuntimeException` if the file did not exist, so we can assume the `::realpath()` call below will not throw an exception. --- src/Composer/Repository/RepositoryFactory.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Composer/Repository/RepositoryFactory.php b/src/Composer/Repository/RepositoryFactory.php index 52da0d604e76..e0026859711c 100644 --- a/src/Composer/Repository/RepositoryFactory.php +++ b/src/Composer/Repository/RepositoryFactory.php @@ -17,7 +17,9 @@ use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\Pcre\Preg; +use Composer\Util\Filesystem; use Composer\Util\HttpDownloader; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Json\JsonFile; @@ -37,7 +39,14 @@ public static function configFromString(IOInterface $io, Config $config, string $json = new JsonFile($repository, Factory::createHttpDownloader($io, $config)); $data = $json->read(); if (!empty($data['packages']) || !empty($data['includes']) || !empty($data['provider-includes'])) { - $repoConfig = ['type' => 'composer', 'url' => 'file://' . strtr(realpath($repository), '\\', '/')]; + $repository = Platform::realpath((new Filesystem())->normalizePath($repository)); + if (!Filesystem::isStreamWrapperPath($repository) && !str_starts_with($repository, 'file://')) { + $repository = 'file://' . $repository; + } + $repoConfig = [ + 'type' => 'composer', + 'url' => $repository, + ]; } elseif ($allowFilesystem) { $repoConfig = ['type' => 'filesystem', 'json' => $json]; } else { From 59bdaec40ddab7264f7308811b921b15a90bc9f0 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Wed, 23 Jul 2025 22:47:35 -0700 Subject: [PATCH 30/40] Use `::realpath()` early on `$targetDir` after `::ensureDirectoryExists()` --- src/Composer/Autoload/AutoloadGenerator.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 13a743979c56..18d66d28b520 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -217,9 +217,10 @@ public function dump(Config $config, InstalledRepositoryInterface $localRepo, Ro $prependAutoloader = $config->get('prepend-autoloader') === false ? 'false' : 'true'; $targetDir = $vendorPath.'/'.$targetDir; $filesystem->ensureDirectoryExists($targetDir); + $targetDir = Platform::realpath($vendorPath.'/'.$targetDir); - $vendorPathCode = $filesystem->findShortestPathCode(Platform::realpath($targetDir), $vendorPath, true); - $vendorPathToTargetDirCode = $filesystem->findShortestPathCode($vendorPath, Platform::realpath($targetDir), true); + $vendorPathCode = $filesystem->findShortestPathCode($targetDir, $vendorPath, true); + $vendorPathToTargetDirCode = $filesystem->findShortestPathCode($vendorPath, $targetDir, true); $appBaseDirCode = $filesystem->findShortestPathCode($vendorPath, $basePath, true); $appBaseDirCode = str_replace('__DIR__', '$vendorDir', $appBaseDirCode); @@ -1162,10 +1163,10 @@ class ComposerStaticInit$suffix $filesystem = new Filesystem(); - $vendorPathCode = ' => ' . $filesystem->findShortestPathCode(Platform::realpath($targetDir), $vendorPath, true, true) . " . '/"; - $vendorPharPathCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode(Platform::realpath($targetDir), $vendorPath, true, true) . " . '/"; - $appBaseDirCode = ' => ' . $filesystem->findShortestPathCode(Platform::realpath($targetDir), $basePath, true, true) . " . '/"; - $appBaseDirPharCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode(Platform::realpath($targetDir), $basePath, true, true) . " . '/"; + $vendorPathCode = ' => ' . $filesystem->findShortestPathCode($targetDir, $vendorPath, true, true) . " . '/"; + $vendorPharPathCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode($targetDir, $vendorPath, true, true) . " . '/"; + $appBaseDirCode = ' => ' . $filesystem->findShortestPathCode($targetDir, $basePath, true, true) . " . '/"; + $appBaseDirPharCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode($targetDir, $basePath, true, true) . " . '/"; $absoluteVendorPathCode = ' => ' . substr(var_export(rtrim($vendorDir, '\\/') . '/', true), 0, -1); $absoluteVendorPharPathCode = ' => ' . substr(var_export(rtrim('phar://' . $vendorDir, '\\/') . '/', true), 0, -1); From dc204f7e8260a19f2dd311ffbe0b5771b24af506 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Wed, 23 Jul 2025 22:55:12 -0700 Subject: [PATCH 31/40] Undo accidental double-prefix of `$targetDir` --- src/Composer/Autoload/AutoloadGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 18d66d28b520..61918bf254e7 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -217,7 +217,7 @@ public function dump(Config $config, InstalledRepositoryInterface $localRepo, Ro $prependAutoloader = $config->get('prepend-autoloader') === false ? 'false' : 'true'; $targetDir = $vendorPath.'/'.$targetDir; $filesystem->ensureDirectoryExists($targetDir); - $targetDir = Platform::realpath($vendorPath.'/'.$targetDir); + $targetDir = Platform::realpath($targetDir); $vendorPathCode = $filesystem->findShortestPathCode($targetDir, $vendorPath, true); $vendorPathToTargetDirCode = $filesystem->findShortestPathCode($vendorPath, $targetDir, true); From 6dd3cf8857ca7eca1e3dab9c3a319cd075a136a4 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Fri, 25 Jul 2025 20:09:38 -0700 Subject: [PATCH 32/40] Add `::testGetPackageTime()` for `realpath()` change --- tests/Composer/Test/Package/LockerTest.php | 119 +++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index 8bc4c598b2d5..7e6db4c87238 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Package; use Composer\Json\JsonFile; +use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Locker; use Composer\Plugin\PluginInterface; use Composer\IO\NullIO; @@ -223,6 +224,124 @@ public function testIsFreshFalseWithContentHash(): void self::assertFalse($locker->isFresh()); } + public static function provideGetPackageTimePath(): array + { + return [ + 'valid_path' => ['path' => __DIR__, 'isValidPath' => true], + 'invalid_path' => ['path' => '/not/a/real/path', 'isValidPath' => false], + ]; + } + + /** + * Test to assert the behaviour of `Locker::getPackageTime()` does not change when replacing `realpath()` with + * `Platform::realpath()`. + * + * @dataProvider provideGetPackageTimePath + * + * @see Platform::realpath() + * + * @covers Locker::getPackageTime + */ + public function testGetPackageTime(string $path, bool $isValidPath): void + { + $jsonLockFile = $this->createJsonFileMock(); + $installationManager = $this->createInstallationManagerMock(); + + $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); + + $locker = new Locker(new NullIO, $jsonLockFile, $installationManager, $this->getJsonContent(), $processExecutor); + + $package = $this->createPackageMock(); + + $package->expects($this->atLeastOnce()) + ->method('getPrettyName') + ->willReturn('package/name'); + + $package->expects($this->atLeastOnce()) + ->method('getPrettyVersion') + ->willReturn('1.0.0'); + + $package->expects($this->once()) + ->method('isDev') + ->willReturn(true); + + $package->expects($this->atLeastOnce()) + ->method('getInstallationSource') + ->willReturn('source'); + + $packages = [$package]; + + $installationManager->expects($this->once()) + ->method('getInstallPath') + ->with($package) + ->willReturn($path); + + $package->expects($this->atLeastOnce()) + ->method('getSourceType') + ->willReturn('git'); + + /** + * @see ArrayDumper::dump() + */ + $package->expects($this->atLeastOnce()) + ->method('getSourceReference') + ->willReturn('git'); + + if ($isValidPath) { + $processExecutor->expects($this->exactly(2)) + ->method('execute') + ->willReturnOnConsecutiveCalls( + $this->returnCallback(function (array $command, ?string &$output1) { + // Using `::withConsecutive()` to assert the parameters was failing. + $this->assertEquals([ 'git', '--version' ], $command); + + $output1 = '2.49.0'; + + return 0; + }), + $this->returnCallback(function ($command, &$output2, ?string $path) { + $this->assertEquals([ + 'git', + 'log', + '-n1', + '--pretty=%ct', + 'git', + ], $command); + $this->assertEquals(__DIR__, $path); + + $output2 = (string) time(); + + return 0; + }) + ); + } else { + $processExecutor->expects($this->never()) + ->method('execute'); + } + + $locker->setLockData( + $packages, + /** devPackages: */ + null, + /** platformReqs: */ + [], + /** platformDevReqs: */ + [], + /** aliases: */ + [], + /** minimumStability: */ + 'dev', + /** stabilityFlags: */ + [], + /** preferStable: */ + false, + /** preferLowest: */ + false, + /** platformOverrides: */ + [] + ); + } + /** * @return \PHPUnit\Framework\MockObject\MockObject&\Composer\Json\JsonFile */ From 8184e1e4806dbc1e5248060ee163ea1a8d9998ea Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Wed, 30 Jul 2025 16:25:01 -0700 Subject: [PATCH 33/40] Add `::testCreatesComposerJsonIfNotExists()` --- .../Test/Command/ConfigCommandTest.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Composer/Test/Command/ConfigCommandTest.php b/tests/Composer/Test/Command/ConfigCommandTest.php index 5542eb828c82..b8ba706ccaa8 100644 --- a/tests/Composer/Test/Command/ConfigCommandTest.php +++ b/tests/Composer/Test/Command/ConfigCommandTest.php @@ -184,4 +184,31 @@ public function testConfigThrowsForInvalidArgCombination(): void $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'config', '--file' => 'alt.composer.json', '--global' => true]); } + + /** + * @covers \Composer\Command\ConfigCommand::initialize + */ + public function testCreatesComposerJsonIfNotExists(): void + { + $testTempDirPath = $this->initTempComposer(); + + $composerDirPath = $testTempDirPath . '/composer-home'; + + mkdir($composerDirPath); + + $composerFilePath = $composerDirPath . '/composer.json'; + + assert(!file_exists($composerFilePath)); + + $appTester = $this->getApplicationTester(); + $appTester->run([ + 'command' => 'global', + 'command-name' => 'config', + '--no-interaction', + ]); + + $appTester->assertCommandIsSuccessful(); + + self::assertFileExists($composerFilePath); + } } From 89d71b26846fb07933a6e492dbac677dc0531237 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Wed, 30 Jul 2025 16:43:10 -0700 Subject: [PATCH 34/40] Add `::testClearCacheCommandDirectoryDoesNotExist()` --- .../Test/Command/ClearCacheCommandTest.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Composer/Test/Command/ClearCacheCommandTest.php b/tests/Composer/Test/Command/ClearCacheCommandTest.php index b76414423273..980edb86e7da 100644 --- a/tests/Composer/Test/Command/ClearCacheCommandTest.php +++ b/tests/Composer/Test/Command/ClearCacheCommandTest.php @@ -58,4 +58,26 @@ public function testClearCacheCommandWithOptionNoCache(): void self::assertStringContainsString('Cache is not enabled', $output); } + + /** + * @covers \Composer\Command\ClearCacheCommand::execute + */ + public function testClearCacheCommandDirectoryDoesNotExist(): void + { + $testTempDirPath = $this->initTempComposer(); + + // Create the expected cache directories, except the `/vcs` directory. + mkdir($testTempDirPath . '/composer-home/cache', 0777, true); + mkdir($testTempDirPath . '/composer-home/cache/repo'); + mkdir($testTempDirPath . '/composer-home/cache/files'); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'clear-cache']); + + $appTester->assertCommandIsSuccessful(); + + $output = $appTester->getDisplay(true); + + self::assertStringContainsString('Cache directory does not exist', $output); + } } From 8bb5b15b884ebe4f4dab8bec4c092a80edc42720 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Wed, 30 Jul 2025 17:10:44 -0700 Subject: [PATCH 35/40] Add `::isInstalled()` tests for symlinks --- tests/Composer/Test/Installer/LibraryInstallerTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php index aa63eb2241ac..5264d97e2283 100644 --- a/tests/Composer/Test/Installer/LibraryInstallerTest.php +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -135,6 +135,16 @@ public function testIsInstalled(): void self::ensureDirectoryExistsAndClear($this->vendorDir.'/'.$package->getPrettyName()); self::assertTrue($library->isInstalled($repository, $package)); + // package in symlinked directory is also seen as installed + $this->fs->removeDirectory($this->vendorDir.'/'.$package->getPrettyName()); + self::ensureDirectoryExistsAndClear($this->vendorDir.'/test/pkg-link-target'); + symlink($this->vendorDir.'/test/pkg-link-target', $this->vendorDir.'/'.$package->getPrettyName()); + self::assertTrue($library->isInstalled($repository, $package)); + + // package in broken symlinked directory is not installed + $this->fs->rmdir($this->vendorDir.'/test/pkg-link-target'); + self::assertFalse($library->isInstalled($repository, $package)); + $repository->removePackage($package); self::assertFalse($library->isInstalled($repository, $package)); } From f3ae074d983570a12674d55eb86df95f1b3fd65d Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Wed, 30 Jul 2025 19:27:15 -0700 Subject: [PATCH 36/40] Improve comment explaining `stream_wrapper_register()` --- tests/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2ddd37bc06bc..f25652154ef2 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -37,7 +37,7 @@ Platform::clearEnv('COMPOSER_BIN_DIR'); /** - * The stream wrapper checks use a static property to store the stream wrapper names. If it is accessed before + * Tests involving stream wrapper checks use a static property to store the stream wrapper names. If it is accessed before * the stream wrapper is registered, and a later test relies on that stream wrapper, the static property will be * outdated and the test will fail. * From 79e9bc500bc215a8caf153208865c9eb8fe873ce Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Wed, 30 Jul 2025 19:46:58 -0700 Subject: [PATCH 37/40] Check the full `.../composer.json` path --- src/Composer/Repository/PathRepository.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index a45298adaaa3..acbab8b516f4 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -165,11 +165,11 @@ protected function initialize(): void foreach ($urlMatches as $url) { try { - $path = Platform::realpath($url) . DIRECTORY_SEPARATOR; + $composerFilePath = $url.DIRECTORY_SEPARATOR.'composer.json'; + $path = Platform::realpath($composerFilePath); } catch (\RuntimeException $exception) { continue; } - $composerFilePath = $path.'composer.json'; $json = file_get_contents($composerFilePath); $package = JsonFile::parseJson($json, $composerFilePath); From 5adaa868c7eb5f696da6a301ff27a779197afddb Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 31 Jul 2025 17:58:02 -0700 Subject: [PATCH 38/40] Use `Platform::getCwd()` --- src/Composer/Command/InitCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 6d8d579d5004..073afc94fd99 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -272,7 +272,7 @@ protected function interact(InputInterface $input, OutputInterface $output) $name = $input->getOption('name'); if (null === $name) { - $cwd = Platform::realpath("."); + $cwd = Platform::getCwd(); $name = basename($cwd); $name = $this->sanitizePackageNameComponent($name); From f2db2ca818015fbf46843b508c07ba72dddf58cd Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 31 Jul 2025 18:10:28 -0700 Subject: [PATCH 39/40] Use `Platform::getCwd()` --- src/Composer/Command/InitCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 073afc94fd99..fbddd11b4045 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -185,7 +185,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { $ignoreFile = Platform::realpath('.gitignore'); } catch (\RuntimeException $exception) { - $ignoreFile = Platform::realpath('.') . '/.gitignore'; + $ignoreFile = Platform::getCwd() . '/.gitignore'; } if (!$this->hasVendorIgnore($ignoreFile)) { From 9d8b1799f651e9870bb238775f54368467dd1676 Mon Sep 17 00:00:00 2001 From: Brian Henry Date: Thu, 31 Jul 2025 18:48:10 -0700 Subject: [PATCH 40/40] Catch more specific `RuntimeException` --- src/Composer/Package/Archiver/ZipArchiver.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Composer/Package/Archiver/ZipArchiver.php b/src/Composer/Package/Archiver/ZipArchiver.php index 430733206feb..b7c16cb89906 100644 --- a/src/Composer/Package/Archiver/ZipArchiver.php +++ b/src/Composer/Package/Archiver/ZipArchiver.php @@ -14,6 +14,7 @@ use Composer\Util\Filesystem; use Composer\Util\Platform; +use RuntimeException; use ZipArchive; /** @@ -35,7 +36,7 @@ public function archive(string $sources, string $target, string $format, array $ try { $sources = Platform::realpath($sources); - } catch (\Exception $exception) { + } catch (RuntimeException $exception) { } $sources = $fs->normalizePath($sources);