8000 Improve stream wrapper support by BrianHenryIE · Pull Request #12396 · composer/composer · GitHub
[go: up one dir, main page]

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

BrianHenryIE
Copy link
@BrianHenryIE BrianHenryIE commented May 2, 2025

realpath() always returns false for stream wrappers. This PR changes:

  • Factory and AutoloadGenerator to use the existing Composer\Util\Platform::realpath() function instead of PHP's realpath()
  • Filesystem::isAbsolutePath() to return true for stream wrappers' paths

See:

PhpStan CI check fails for SuggestedPackagesReporter.php which is not related to this PR.

Edit:

There are an additional ~58 uses of regular realpath() in the code (grep -rn src -e "[^t:]realpath(" | grep -v '//' | grep -v "\s*\*" | wc -l), across 30 files (grep -rl src -e "[^t:]realpath(" | wc -l). An okay find & replace regex for PhpStorm is (^((?!//|\*).)*(?<!function |get|::|>))(realpath\(), $1Platform::$3 but it has some false positives, particularly BinaryInstaller's $globalsCode string, and the files may need use Composer\Util\Platform; added.

And in AutoloadGenerator:

// 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())));

Maybe the double-realpath call should be moved into Platform::realpath() and the Platform::getCwd() should always return its realpath()?

@BrianHenryIE BrianHenryIE marked this pull request as ready for review May 2, 2025 07:27
return strpos($path, '/') === 0
|| substr($path, 1, 1) === ':'
|| strpos($path, '\\\\') === 0
|| Preg::isMatch($this->streamWrappersRegex, $path);
Copy link
Member

Choose a reason for hiding this comment

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

I guess this should be used inside Platform::realpath for the PR to have any effect?

Copy link
Author

Choose a reason for hiding this comment

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

No. This change is for places where a non-absolute path is prepended with the working dir, e.g.

$dir = $filesystem->normalizePath($filesystem->isAbsolutePath($dir) ? $dir : $basePath.'/'.$dir);

which is unnecessary for stream paths.

That said, I do need to look closer at where it is used, e.g.

if ($fs->isAbsolutePath($url)) {
return 'file://' . strtr($url, '\\', '/');
}

@Seldaek
Copy link
Member
Seldaek commented May 12, 2025

Maybe the double-realpath call should be moved into Platform::realpath() and the Platform::getCwd() should always return its realpath()?

Per the linked bug yes maybe we should always realpath n+1 times on windows, for as long as we get a new path, to make sure we resolve junctions.. On linux the double realpath is useless.

As for Platform::getCwd I do not think it should realpath, as you don't always want to realpath it AFAIR.

There are an additional ~58 uses of regular realpath()

I guess those could be modified too for consistency.

@Seldaek Seldaek added this to the 2.9 milestone May 12, 2025
@@ -56,13 +56,19 @@ 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
{
$realPath = realpath($path);
if ($realPath === false) {
return $path;
Copy link
Author
@BrianHenryIE BrianHenryIE May 13, 2025

Choose a reason for hiding this comment

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

Suggested change
return $path;
return file_exists($realPath) || is_dir($realpath);

I was applying the realpath() to Platform::realpath() change across the other cases, and they are sometimes followed by if($realpathResult === false){ } blocks. E.g.

$sourcesRealPath = realpath($sources);
if ($sourcesRealPath === false) {
throw new \RuntimeException('Could not realpath() the source directory "'.$sources.'"');
}

I'm thinking Platform::realpath() should return false if the path is incorrect, which is inherent in a realpath('/does/not/exist') call, and which the stream wrapper should handle correctly.

(and revert 198a7ea)

I need to look at the original reason Platform::realpath() was introduced: nicolas-grekas@3d1fb9d

Any suggestion on writing tests on this? I often use antecedent/patchwork which allows redefining built in PHP functions. Although I see composer/composer is using PHPUnit in an unconventional way.

I have applied Platform::realpath() wherever it was not followed by a false check: d1081a3

Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking Platform::realpath() should return false if the path is incorrect, which is inherent in a realpath('/does/not/exist') call, and which the stream wrapper should handle correctly.

Yes, this complicates Platform::realpath() usage.. Ideally we avoid returning false, because checking for it everywhere is a pain. I can see with stream wrappers it's probably more likely to return false, but what are conditions where they would do that? Can we recover from this or should we just throw?

The fallback to return $path; that we currently have in Platform::realpath is because in theory realpath shouldn't fail when the path exists, and we don't call it if the path doesn't exist AFAIK. I have never been able to conclusively say what might trigger a false otherwise, so I put this fallback in place and nobody ever complained so either it isn't used or it works 🤷🏻‍♂️ .

I'd probably be fine throwing in case false is returned, and then we see what happens in the real world, if it causes issues we can still revert to falling back for non-stream-wrapper paths I guess to restore the previous behavior.

Stream wrapper usage is definitely very fringe, so I am a bit worried of degrading the overall experience just to fix this.

Any suggestion on writing tests on this? I often use antecedent/patchwork which allows redefining built in PHP functions. Although I see composer/composer is using PHPUnit in an unconventional way.

You should be able to use patchwork as it requires php 7.1+, so just give it a shot I guess..

if (false === $resolvedPath) {
continue;
}
$resolvedPath = Platform::realpath($installPath . '/' . $updir);
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 is a special case where we aren't checking file_exists before, and thus using the false return value as indication that the path doesn't exist and can be skipped.

So I'd probably add a file_exists check before, assuming realpath is changed to throw on failure. Or try/catch to be safer perhaps, because I suppose even if the file exists it might still throw in some weird conditions.

@@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

This one should be moved below the file_exists check that happens after realpath..

@@ -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.

*/
public static function realpath(string $path): string
{
$realPath = realpath($path);
if ($realPath === false) {
return $path;
}
if ($realPath !== $path) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if ($realPath !== $path) {
if (self::isWindows() && $realPath !== $path) {

Co-authored-by: Jordi Boggiano <j.boggiano@seld.be>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0