8000 Improve stream wrapper support by BrianHenryIE · Pull Request #12396 · composer/composer · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Improve stream wrapper support #12396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2ab3d1e
use `Composer\Util\Platform::realpath()`
BrianHenryIE May 2, 2025
708c809
use `Composer\Util\Platform::realpath()`
BrianHenryIE May 2, 2025
e8a516f
Always treat stream wrappers as absolute paths
BrianHenryIE May 2, 2025
198a7ea
Remove `false` check which no longer can evaluate to false
BrianHenryIE May 2, 2025
9f03eba
Add `::testIsAbsolutePath()`
BrianHenryIE May 2, 2025
ed968c0
Do not treat `file://` paths as absolute path without check it un-pre…
BrianHenryIE May 13, 2025
27fac4b
Recursively call `::realpath()` until `$path` does not change
BrianHenryIE May 13, 2025
d1081a3
Replace `realpath()` with `Platform::realpath()`
BrianHenryIE May 13, 2025
4b5129b
Update src/Composer/Util/Filesystem.php
BrianHenryIE May 13, 2025
21b7a7e
Refactor `::isStreamWrapperPath()` to `public static` method
BrianHenryIE Jun 6, 2025
a23e7ce
Do not treat `file://` as a streamwrapper
BrianHenryIE Jun 6, 2025
40eb099
Return existing path for streamwrapper; throw exception for invalid path
BrianHenryIE Jun 6, 2025
0cd95aa
Move `stream_wrapper_register()` to `bootstrap.php`
BrianHenryIE Jun 6, 2025
598e88e
Replace `realpath()` with `Platform::realpath()`
BrianHenryIE Jun 12, 2025
413dd88
Add test `::realpath()` throws `RuntimeException`
BrianHenryIE Jul 3, 2025
d859488
Add `::testIsStreamWrapperPath()`
BrianHenryIE Jul 3, 2025
7542d71
Add `::testIsLocalPath()`
BrianHenryIE Jul 3, 2025
faeac42
Remove outdated redundant throw
BrianHenryIE Jul 4, 2025
aa70941
Return `false` on `::realpath()` `RuntimeException`
BrianHenryIE Jul 4, 2025
1430918
`continue` on failed `::realpath()`
BrianHenryIE Jul 4, 2025
1baf8bd
Strip `file://` before running system `realpath()`
BrianHenryIE Jul 4, 2025
2c9ed1f
Add `::testRealPathFileStreamStripsScheme()`
BrianHenryIE Jul 4, 2025
d32e015
Update `::isStreamWrapperPath()
BrianHenryIE Jul 4, 2025
463cebe
Don't `file://` prefix stream paths
BrianHenryIE Jul 4, 2025
32e6c08
No need to catch and throw `RuntimeException` here, the message was fine
BrianHenryIE Jul 4, 2025
d359604
Preserve `null` or `realpath()`
BrianHenryIE Jul 4, 2025
6592d1d
Update PhpDoc arguing we how we treat stream wrappers in `::isLocalPa…
BrianHenryIE Jul 4, 2025
95353bc
Preserver behaviour; redundant throw
BrianHenryIE Jul 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions src/Composer/Autoload/AutoloadGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,18 +211,15 @@ 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(realpath(realpath(Platform::getCwd())));
$vendorPath = $filesystem->normalizePath(realpath(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;
$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);
Expand Down Expand Up @@ -493,7 +490,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);
Expand Down Expand Up @@ -1165,10 +1162,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);
Expand Down Expand Up @@ -1291,10 +1288,12 @@ static function ($matches) use (&$updir): string {
$installPath = strtr(Platform::getCwd(), '\\', '/');
}

$resolvedPath = realpath($installPath . '/' . $updir);
if (false === $resolvedPath) {
try {
$resolvedPath = Platform::realpath($installPath . '/' . $updir);
} catch (\RuntimeException $e) {
continue;
}

$autoloads[] = preg_quote(strtr($resolvedPath, '\\', '/')) . '/' . $path . '($|/)';
continue;
}
Expand Down
6 changes: 4 additions & 2 deletions src/Composer/Command/ClearCacheCommand.php

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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("<info>Cache directory does not exist ($key): $cachePath</info>");

continue;
Expand Down
2 changes: 1 addition & 1 deletion src/Composer/Command/ConfigCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
4 changes: 3 additions & 1 deletion src/Composer/Command/CreateProjectCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions src/Composer/Command/InitCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);

Expand Down
20 changes: 12 additions & 8 deletions src/Composer/Command/ShowCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -903,7 +904,11 @@ protected function printMeta(CompletePackageInterface $package, array $versions,
if ($isInstalledPackage) {
$path = $this->requireComposer()->getInstallationManager()->getInstallPath($package);
if (is_string($path)) {
$io->write('<info>path</info> : ' . realpath($path));
try {
$io->write('<info>path</info> : ' . Platform::realpath($path));
} catch (\RuntimeException $exception) {
$io->write('<info>path</info> : ');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This preserves current behaviour but isn't great IMO.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'd say that is probably ok to expect the realpath to work here and remove the catch.

}
} else {
$io->write('<info>path</info> : null');
}
Expand Down Expand Up @@ -1063,13 +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 = realpath($path);
if ($path !== false) {
$json['path'] = $path;
try {
$json['path'] = Platform::realpath($path);
} catch (\RuntimeException $exception) {
}
} else {
$json['path'] = null;
}

if ($package->getReleaseDate() !== null) {
Expand Down
6 changes: 4 additions & 2 deletions src/Composer/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -142,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] = 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);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Composer/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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') ?: '/');
Copy link
Member
@Seldaek Seldaek May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one too I'd probably try/catch and use $home = Platform::realpath('/') as fallback. Because otherwise if HOME points to an invalid dir somehow this will blow up in bad ways for people, and this is a fairly hot code path so I'd rather not risk major issues.


// abort when we reach the home dir or top of the filesystem
while (dirname($dir) !== $dir && $dir !== $home) {
Expand Down Expand Up @@ -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'])) {
Expand Down
10 changes: 4 additions & 6 deletions src/Composer/Downloader/ArchiveDownloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -89,14 +89,12 @@ 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);
$realpath = realpath($path);
if ($realpath !== false) {
$this->removeCleanupPath($package, $realpath);
}
$realpath = Platform::realpath($path);
$this->removeCleanupPath($package, $realpath);
};

try {
Expand Down
2 changes: 1 addition & 1 deletion src/Composer/Downloader/FileDownloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Composer/Downloader/GitDownloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions src/Composer/Downloader/HgDownloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace Composer\Downloader;

use Composer\Util\Platform;
use React\Promise\PromiseInterface;
use Composer\Package\PackageInterface;
use Composer\Util\ProcessExecutor;
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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);

Expand All @@ -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());
}

Expand Down
26 changes: 11 additions & 15 deletions src/Composer/Downloader/PathDownloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,30 @@ 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,
$package->getName()
));
}

if (realpath($path) === $realUrl) {
$realPath = Platform::realpath($path);
if ($realPath === $realUrl) {
return \React\Promise\resolve(null);
}

if (strpos(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
// for previous attempts that were shut down because they did not work well enough or introduced too many risks.
throw new \RuntimeException(sprintf(
'Package %s cannot install to "%s" inside its source at "%s"',
$package->getName(),
realpath($path),
$realPath,
$realUrl
));
}
Expand All @@ -85,12 +87,9 @@ 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);
if (false === $realUrl) {
throw new \RuntimeException('Failed to realpath '.$url);
}
$realUrl = Platform::realpath($url);

if (realpath($path) === $realUrl) {
if (Platform::realpath($path) === $realUrl) {
if ($output) {
$this->io->writeError(" - " . InstallOperation::format($package) . $this->getInstallOperationAppendix($package, $path));
}
Expand Down Expand Up @@ -245,12 +244,9 @@ 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 (realpath($path) === $realUrl) {
if (Platform::realpath($path) === $realUrl) {
return ': Source already present';
}

Expand Down
Loading
0