Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions spec/Collection/ProcessArgumentsCollectionSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,35 @@ function it_should_be_able_to_add_files()
]);
}

function it_should_prevent_files_from_being_parsed_as_options()
{
$files = new FilesCollection([
new SplFileInfo('--config=file1.php'),
new SplFileInfo('-c.php'),
new SplFileInfo('-dir/file2.php'),
new SplFileInfo('dir/-file3.php'),
]);
$this->addFiles($files);

$this->getValues()->shouldBe([
'./--config=file1.php',
'./-c.php',
'./-dir/file2.php',
'dir/-file3.php',
]);
}

function it_should_prevent_comma_separated_files_from_being_parsed_as_options()
{
$files = new FilesCollection([
new SplFileInfo('--config=file1.php'),
new SplFileInfo('file2.php')
]);
$this->addCommaSeparatedFiles($files);

$this->getValues()->shouldBe(['./--config=file1.php,file2.php']);
}

function it_should_be_able_to_add_comma_separated_files()
{
$files = new FilesCollection([
Expand Down
15 changes: 13 additions & 2 deletions src/Collection/ProcessArgumentsCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,31 @@ public function addFiles(FilesCollection $files): void

public function addFile(\SplFileInfo $file): void
{
$this->add($file->getPathname());
$this->add(self::escapeFilePath($file));
}

public function addCommaSeparatedFiles(FilesCollection $files): void
{
$paths = [];

foreach ($files as $file) {
$paths[] = $file->getPathname();
$paths[] = self::escapeFilePath($file);
}

$this->add(implode(',', $paths));
}

/**
* A repository file named like "--config=evil.php" would otherwise be parsed as an option by the tool.
* Prefixing it with "./" keeps it a path, without relying on every tool supporting the "--" separator.
*/
private static function escapeFilePath(\SplFileInfo $file): string
{
$path = $file->getPathname();

return str_starts_with($path, '-') ? './'.$path : $path;
}

public function addArgumentWithCommaSeparatedFiles(string $argument, FilesCollection $files): void
{
$paths = [];
Expand Down
2 changes: 2 additions & 0 deletions src/Task/Git/Blacklist.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ public function run(ContextInterface $context): TaskResultInterface
}

$arguments = $this->processBuilder->createArgumentsForCommand('git');
// Without this, a committed file named like ":!other.php" is read as pathspec magic and excludes files.
$arguments->add('--literal-pathspecs');
$arguments->add('grep');
$arguments->add('--cached');
$arguments->add('-n');
Expand Down
32 changes: 32 additions & 0 deletions test/E2E/AbstractE2ETestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,38 @@ protected function enableDummyTask(string $grumphpFile, string $projectDir, arra
]);
}

protected function enableValidateArgvPathsTask(string $grumphpFile, string $projectDir)
{
$e2eDir = $this->ensureGrumphpE2eTasksDir($projectDir);
$this->dumpFile(
$e2eDir.'/ValidateArgvPathsTask.php',
file_get_contents(TEST_BASE_PATH.'/fixtures/e2e/tasks/ValidateArgvPathsTask.php')
);
$this->dumpFile(
$e2eDir.'/validate-argv-paths.php',
file_get_contents(TEST_BASE_PATH.'/fixtures/e2e/tasks/validate-argv-paths.php')
);

$this->mergeGrumphpConfig($grumphpFile, [
'grumphp' => [
'tasks' => [
'validateArgvPaths' => [],
],
],
'services' => [
'GrumPHPE2E\\ValidateArgvPathsTask' => [
'arguments' => ['@process_builder'],
'tags' => [
[
'name' => 'grumphp.task',
'task' => 'validateArgvPaths'
],
]
]
],
]);
}

protected function installComposer(string $path, array $arguments = [])
{
$process = new Process(
Expand Down
57 changes: 57 additions & 0 deletions test/E2E/TasksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,61 @@ function it_can_resolve_task_config_With_env_vars()
$this->commitAll();
$this->runGrumphp($this->rootDir);
}

#[Test]
function it_passes_option_like_file_names_to_external_commands_as_paths()
{
$this->initializeGitInRootDir();
$this->initializeComposer($this->rootDir);
$grumphpFile = $this->initializeGrumphpConfig($this->rootDir);
$this->installComposer($this->rootDir);
$this->ensureHooksExist();

$this->enableValidateArgvPathsTask($grumphpFile, $this->rootDir);
$this->dumpOptionLikeFiles();

$this->commitAll();
$this->runGrumphp($this->rootDir);
}

#[Test]
function it_finds_blacklisted_keywords_in_option_like_file_names()
{
$this->initializeGitInRootDir();
$this->initializeComposer($this->rootDir);
$grumphpFile = $this->initializeGrumphpConfig($this->rootDir);
$this->installComposer($this->rootDir);
$this->ensureHooksExist();

$this->mergeGrumphpConfig($grumphpFile, [
'grumphp' => [
'tasks' => [
'git_blacklist' => [
'keywords' => ['blacklisted_keyword'],
],
],
],
]);
$this->dumpOptionLikeFiles();
$this->dumpFile($this->rootDir.'/-dash.php', '<?php // blacklisted_keyword'.PHP_EOL);

try {
$this->commitAll();
} catch (\RuntimeException $e) {
$this->assertStringContainsString('You have blacklisted keywords in your commit', $e->getMessage());
$this->assertStringContainsString('-dash.php', $e->getMessage());

return;
}

$this->fail('Expected git_blacklist to find the keyword in -dash.php.');
}

private function dumpOptionLikeFiles(): void
{
$this->dumpFile($this->rootDir.'/--option-like=value.php', '<?php'.PHP_EOL);
$this->dumpFile($this->rootDir.'/-dash.php', '<?php'.PHP_EOL);
$this->mkdir($this->rootDir.'/-dir');
$this->dumpFile($this->rootDir.'/-dir/file.php', '<?php'.PHP_EOL);
}
}
3 changes: 3 additions & 0 deletions test/Unit/Task/Git/BlacklistTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ public static function provideExternalTaskRuns(): iterable
self::mockContext(RunContext::class, ['hello.php', 'hello2.php']),
'git',
[
'--literal-pathspecs',
'grep',
'--cached',
'-n',
Expand All @@ -175,6 +176,7 @@ public static function provideExternalTaskRuns(): iterable
self::mockContext(RunContext::class, ['hello.php', 'hello2.php']),
'git',
[
'--literal-pathspecs',
'grep',
'--cached',
'-n',
Expand All @@ -198,6 +200,7 @@ public static function provideExternalTaskRuns(): iterable
self::mockContext(RunContext::class, ['hello.php', 'hello2.php']),
'git',
[
'--literal-pathspecs',
'grep',
'--cached',
'-n',
Expand Down
70 changes: 70 additions & 0 deletions test/fixtures/e2e/tasks/ValidateArgvPathsTask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php
namespace GrumPHPE2E;

use GrumPHP\Process\ProcessBuilder;
use GrumPHP\Runner\TaskResult;
use GrumPHP\Runner\TaskResultInterface;
use GrumPHP\Task\Config\ConfigOptionsResolver;
use GrumPHP\Task\Config\EmptyTaskConfig;
use GrumPHP\Task\Config\TaskConfigInterface;
use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\TaskInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ValidateArgvPathsTask implements TaskInterface
{
/**
* @var TaskConfigInterface
*/
private $config;

/**
* @var ProcessBuilder
*/
private $processBuilder;

public function __construct(ProcessBuilder $processBuilder)
{
$this->config = new EmptyTaskConfig();
$this->processBuilder = $processBuilder;
}

public function getConfig(): TaskConfigInterface
{
return $this->config;
}

public function withConfig(TaskConfigInterface $config): TaskInterface
{
$new = clone $this;
$new->config = $config;

return $new;
}

public static function getConfigurableOptions(): ConfigOptionsResolver
{
return ConfigOptionsResolver::fromOptionsResolver(new OptionsResolver());
}

public function canRunInContext(ContextInterface $context): bool
{
return true;
}

public function run(ContextInterface $context): TaskResultInterface
{
$arguments = $this->processBuilder->createArgumentsForCommand('php');
$arguments->add(__DIR__.'/validate-argv-paths.php');
$arguments->addFiles($context->getFiles());

$process = $this->processBuilder->buildProcess($arguments);
$process->run();

if (!$process->isSuccessful()) {
return TaskResult::createFailed($this, $context, $process->getOutput().$process->getErrorOutput());
}

return TaskResult::createPassed($this, $context);
}
}
17 changes: 17 additions & 0 deletions test/fixtures/e2e/tasks/validate-argv-paths.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

$errors = [];
foreach (array_slice($argv, 1) as $path) {
if (str_starts_with($path, '-')) {
$errors[] = 'Received a file that a CLI tool would parse as an option: '.$path;
continue;
}

if (!is_file($path)) {
$errors[] = 'Received a path that is not a file: '.$path;
}
}

if ($errors) {
throw new RuntimeException(implode(PHP_EOL, $errors));
}
Loading