-
-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
431 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Shell\Exception; | ||
|
||
use Psl\Exception; | ||
|
||
interface ExceptionInterface extends Exception\ExceptionInterface | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Shell\Exception; | ||
|
||
use Psl\Str; | ||
|
||
final class FailedExecutionException extends RuntimeException implements ExceptionInterface | ||
{ | ||
private string $command; | ||
|
||
private string $stdoutContent; | ||
private string $stderrContent; | ||
|
||
public function __construct(string $command, string $stdout_content, string $stderr_content, int $code) | ||
{ | ||
$message = Str\format('Shell command "%s" returned an exit code of "%d".', $command, $code); | ||
|
||
parent::__construct($message, $code); | ||
|
||
$this->command = $command; | ||
$this->stdoutContent = $stdout_content; | ||
$this->stderrContent = $stderr_content; | ||
} | ||
|
||
/** | ||
* @psalm-mutation-free | ||
*/ | ||
public function getCommand(): string | ||
{ | ||
return $this->command; | ||
} | ||
|
||
/** | ||
* @psalm-mutation-free | ||
*/ | ||
public function getOutput(): string | ||
{ | ||
return $this->stdoutContent; | ||
} | ||
|
||
/** | ||
* @psalm-mutation-free | ||
*/ | ||
public function getErrorOutput(): string | ||
{ | ||
return $this->stderrContent; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Shell\Exception; | ||
|
||
final class PossibleAttackException extends RuntimeException implements ExceptionInterface | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Shell\Exception; | ||
|
||
use Psl\Exception; | ||
|
||
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Shell; | ||
|
||
use Psl\Regex; | ||
use Psl\Str\Byte; | ||
|
||
use const DIRECTORY_SEPARATOR; | ||
|
||
/** | ||
* Escape a string to be used as a shell argument. | ||
* | ||
* @psalm-taint-escape shell | ||
*/ | ||
function escape_argument(string $argument): string | ||
{ | ||
/** | ||
* The following code was copied ( with modification ) from the Symfony Process Component (v5.2.3 - 2021-02-22) | ||
* | ||
* https://github.com/symfony/process/blob/b8d6eff26e48187fed15970799f4b605fa7242e4/Process.php#L1623-L1643 | ||
* | ||
* @license MIT | ||
* @see https://github.com/symfony/process/blob/b8d6eff26e48187fed15970799f4b605fa7242e4/LICENSE | ||
* | ||
* @copyright (c) 2004-2021 Fabien Potencier <fabien@symfony.com> | ||
*/ | ||
if ('' === $argument) { | ||
return '""'; | ||
} | ||
|
||
if ('\\' !== DIRECTORY_SEPARATOR) { | ||
$argument = Byte\replace($argument, "'", "'\\''"); | ||
|
||
return "'" . $argument . "'"; | ||
} | ||
|
||
// @codeCoverageIgnoreStart | ||
/** @psalm-suppress MissingThrowsDocblock - safe ( $offset is within-of-bounds ) */ | ||
if (Byte\contains($argument, "\0")) { | ||
$argument = Byte\replace($argument, "\0", '?'); | ||
} | ||
|
||
/** @psalm-suppress MissingThrowsDocblock - safe ( $pattern is valid ) */ | ||
if (!Regex\matches($argument, '/[\/()%!^"<>&|\s]/')) { | ||
return $argument; | ||
} | ||
|
||
/** @psalm-suppress MissingThrowsDocblock - safe ( $pattern is valid ) */ | ||
$argument = Regex\replace($argument, '/(\\\\+)$/', '$1$1'); | ||
$argument = Byte\replace_every($argument, [ | ||
'"' => '""', | ||
'^' => '"^^"', | ||
'%' => '"^%"', | ||
'!' => '"^!"', | ||
"\n" => '!LF!' | ||
]); | ||
|
||
return '"' . $argument . '"'; | ||
// @codeCoverageIgnoreEnd | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Shell; | ||
|
||
use function escapeshellcmd; | ||
|
||
/** | ||
* Escape shell metacharacters. | ||
* | ||
* @psalm-taint-escape shell | ||
*/ | ||
function escape_command(string $argument): string | ||
{ | ||
return escapeshellcmd($argument); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Shell; | ||
|
||
use Psl\Dict; | ||
use Psl\Env; | ||
use Psl\Str; | ||
use Psl\Vec; | ||
|
||
use function fclose; | ||
use function is_dir; | ||
use function is_resource; | ||
use function proc_close; | ||
use function proc_open; | ||
use function stream_get_contents; | ||
|
||
/** | ||
* Execute an external program. | ||
* | ||
* @param string $command The command to execute. | ||
* @param list<string> $arguments The command arguments listed as separate entries. | ||
* @param string $working_directory The initial working directory for the command. | ||
* This must be an absolute directory path, or null | ||
* if you want to use the default value (the working dir of the | ||
* current directory ) | ||
* @param array<string, string> $environment An array with the environment variables for the command that | ||
* will be run. | ||
* @param bool $escape_arguments If set to true ( default ), all $arguments will be escaped using | ||
* `Shell\escape_argument`. | ||
* | ||
* @psalm-taint-sink shell $command | ||
* | ||
* @throws Exception\FailedExecutionException In case the command resulted in an exit code other than 0. | ||
* @throws Exception\PossibleAttackException In case the command being run is suspicious ( e.g: contains NULL byte ). | ||
* @throws Exception\RuntimeException In case $working_directory doesn't exist, or unable to create a new | ||
* process. | ||
*/ | ||
function execute( | ||
string $command, | ||
array $arguments = [], | ||
?string $working_directory = null, | ||
array $environment = [], | ||
bool $escape_arguments = true | ||
): string { | ||
if ($escape_arguments) { | ||
$arguments = Vec\map($arguments, static fn(string $argument): string => escape_argument($argument)); | ||
} | ||
|
||
$commandline = Str\join([$command, ...$arguments], ' '); | ||
|
||
/** @psalm-suppress MissingThrowsDocblock - safe ( $offset is within-of-bounds ) */ | ||
if (Str\contains($commandline, "\0")) { | ||
throw new Exception\PossibleAttackException('NULL byte detected.'); | ||
} | ||
|
||
$descriptor = [ | ||
1 => ['pipe', 'w'], | ||
2 => ['pipe', 'w'], | ||
]; | ||
|
||
$environment = Dict\merge(Env\get_vars(), $environment); | ||
$working_directory = $working_directory ?? Env\current_dir(); | ||
if (!is_dir($working_directory)) { | ||
throw new Exception\RuntimeException('$working_directory does not exist.'); | ||
} | ||
|
||
$process = proc_open($commandline, $descriptor, $pipes, $working_directory, $environment); | ||
// @codeCoverageIgnoreStart | ||
// not sure how to replicate this, but it can happen \_o.o_/ | ||
if (!is_resource($process)) { | ||
throw new Exception\RuntimeException('Failed to open a new process.'); | ||
} | ||
// @codeCoverageIgnoreEnd | ||
|
||
$stdout_content = stream_get_contents($pipes[1]); | ||
$stderr_content = stream_get_contents($pipes[2]); | ||
|
||
fclose($pipes[1]); | ||
fclose($pipes[2]); | ||
|
||
$code = proc_close($process); | ||
if ($code !== 0) { | ||
throw new Exception\FailedExecutionException($commandline, $stdout_content, $stderr_content, $code); | ||
} | ||
|
||
return $stdout_content; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Tests\Shell; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Psl\Shell; | ||
|
||
final class EscapeArgumentTest extends TestCase | ||
{ | ||
/** | ||
* @dataProvider provideData | ||
*/ | ||
public function testEscapeArgument(string $argument): void | ||
{ | ||
$output = Shell\execute(PHP_BINARY, ['-r', 'echo $argv[1];', $argument]); | ||
|
||
static::assertSame($argument, $output); | ||
} | ||
|
||
/** | ||
* @return iterable<array{0: string}> | ||
*/ | ||
public function provideData(): iterable | ||
{ | ||
yield ['a"b%c%']; | ||
yield ['a"b^c^']; | ||
yield ["a\nb'c"]; | ||
yield ['a^b c!']; | ||
yield ["a!b\tc"]; | ||
yield ["look up ^"]; | ||
yield ['a\\\\"\\"']; | ||
yield ['éÉèÈàÀöä']; | ||
yield ['1']; | ||
yield ['1.1']; | ||
yield ['1%2']; | ||
yield ["Hey there,\nHow are you doing!"]; | ||
yield ['']; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Tests\Shell; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Psl\Shell; | ||
|
||
final class EscapeCommandTest extends TestCase | ||
{ | ||
public function testEscapeCommand(): void | ||
{ | ||
static::assertSame( | ||
"Hello, World!", | ||
Shell\execute(Shell\escape_command(PHP_BINARY), ['-r', 'echo "Hello, World!";']) | ||
); | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
tests/Psl/Shell/Exception/FailedExecutionExceptionTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Tests\Shell\Exception; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Psl\Shell\Exception; | ||
|
||
final class FailedExecutionExceptionTest extends TestCase | ||
{ | ||
public function testMethods(): void | ||
{ | ||
$exception = new Exception\FailedExecutionException('foo', 'bar', 'baz', 4); | ||
|
||
static::assertSame('Shell command "foo" returned an exit code of "4".', $exception->getMessage()); | ||
static::assertSame('foo', $exception->getCommand()); | ||
static::assertSame('bar', $exception->getOutput()); | ||
static::assertSame('baz', $exception->getErrorOutput()); | ||
static::assertSame(4, $exception->getCode()); | ||
} | ||
} |
Oops, something went wrong.